diff options
10 files changed, 65 insertions, 370 deletions
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/AppPermissionId.kt b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/AppPermissionId.kt deleted file mode 100644 index 061bcb8a3..000000000 --- a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/AppPermissionId.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.permissioncontroller.permission.model.livedatatypes.v31 - -import android.os.UserHandle - -/** Identifier for an app permission group combination. */ -data class AppPermissionId( - val packageName: String, - val userHandle: UserHandle, - val permissionGroup: String, -) diff --git a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/LightHistoricalPackageOps.kt b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/LightHistoricalPackageOps.kt deleted file mode 100644 index 04cc8a796..000000000 --- a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/LightHistoricalPackageOps.kt +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.permissioncontroller.permission.model.livedatatypes.v31 - -import android.app.AppOpsManager.AttributedHistoricalOps -import android.app.AppOpsManager.AttributedOpEntry -import android.app.AppOpsManager.HistoricalOp -import android.app.AppOpsManager.HistoricalPackageOps -import android.app.AppOpsManager.OP_FLAG_SELF -import android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED -import android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY -import android.app.AppOpsManager.OpEventProxyInfo -import android.os.UserHandle -import com.android.permissioncontroller.permission.utils.PermissionMapping.getPlatformPermissionGroupForOp - -/** - * Light version of [HistoricalPackageOps] class, tracking the last permission access for system - * permission groups. - */ -data class LightHistoricalPackageOps( - /** Name of the package. */ - val packageName: String, - /** [UserHandle] running the package. */ - val userHandle: UserHandle, - /** - * Data about permission accesses, one [AppPermissionDiscreteAccesses] for each permission - * group. - */ - // TODO(b/262042582): Consider removing this field and using attributed accesses aggregated over - // attribution tags instead. - val appPermissionDiscreteAccesses: List<AppPermissionDiscreteAccesses>, - /** - * Attributed data about permission accesses, one [AttributedAppPermissionDiscreteAccesses] for - * each permission group. - */ - val attributedAppPermissionDiscreteAccesses: List<AttributedAppPermissionDiscreteAccesses> -) { - constructor( - historicalPackageOps: HistoricalPackageOps, - userHandle: UserHandle, - opNames: Set<String> - ) : this( - historicalPackageOps.packageName, - userHandle, - historicalPackageOps.getAppPermissionDiscreteAccesses(userHandle, opNames), - historicalPackageOps.getAttributedAppPermissionDiscreteAccesses(userHandle, opNames), - ) - - /** Companion object for [LightHistoricalPackageOps]. */ - companion object { - /** String to represent the absence of an attribution tag. */ - const val NO_ATTRIBUTION_TAG = "no_attribution_tag" - /** String to represent the absence of a permission group. */ - private const val NO_PERM_GROUP = "no_perm_group" - private const val DISCRETE_ACCESS_OP_FLAGS = - OP_FLAG_SELF or OP_FLAG_TRUSTED_PROXIED or OP_FLAG_TRUSTED_PROXY - - /** - * Creates a list of [AppPermissionDiscreteAccesses] for the provided package, user and ops. - */ - private fun HistoricalPackageOps.getAppPermissionDiscreteAccesses( - userHandle: UserHandle, - opNames: Set<String> - ): List<AppPermissionDiscreteAccesses> { - val permissionsToOpNames = partitionOpsByPermission(opNames) - val appPermissionDiscreteAccesses = mutableListOf<AppPermissionDiscreteAccesses>() - for (permissionToOpNames in permissionsToOpNames.entries) { - this.getDiscreteAccesses(permissionToOpNames.value)?.let { - appPermissionDiscreteAccesses.add( - AppPermissionDiscreteAccesses( - AppPermissionId(packageName, userHandle, permissionToOpNames.key), - it - ) - ) - } - } - - return appPermissionDiscreteAccesses - } - - /** - * Creates a list of [AttributedAppPermissionDiscreteAccesses] for the provided package, - * user and ops. - */ - private fun HistoricalPackageOps.getAttributedAppPermissionDiscreteAccesses( - userHandle: UserHandle, - opNames: Set<String> - ): List<AttributedAppPermissionDiscreteAccesses> { - val permissionsToOpNames = partitionOpsByPermission(opNames) - val attributedAppPermissionDiscreteAccesses = - mutableMapOf<AppPermissionId, MutableMap<String, List<DiscreteAccess>>>() - - val attributedHistoricalOpsList = mutableListOf<AttributedHistoricalOps>() - for (i in 0 until attributedOpsCount) { - attributedHistoricalOpsList.add(getAttributedOpsAt(i)) - } - - for (permissionToOpNames in permissionsToOpNames.entries) { - attributedHistoricalOpsList.forEach { attributedHistoricalOps -> - attributedHistoricalOps.getDiscreteAccesses(permissionToOpNames.value)?.let { - discAccessData -> - val appPermissionId = - AppPermissionId(packageName, userHandle, permissionToOpNames.key) - if (!attributedAppPermissionDiscreteAccesses.containsKey(appPermissionId)) { - attributedAppPermissionDiscreteAccesses[appPermissionId] = - mutableMapOf() - } - attributedAppPermissionDiscreteAccesses[appPermissionId]?.put( - attributedHistoricalOps.tag ?: NO_ATTRIBUTION_TAG, - discAccessData - ) - } - } - } - - return attributedAppPermissionDiscreteAccesses.map { - AttributedAppPermissionDiscreteAccesses(it.key, it.value) - } - } - - /** - * Retrieves all discrete accesses for the provided op names, if any. - * - * Returns null if there are no accesses. - */ - private fun HistoricalPackageOps.getDiscreteAccesses( - opNames: List<String> - ): List<DiscreteAccess>? { - if (opCount == 0) { - return null - } - - val historicalOps = mutableListOf<HistoricalOp>() - for (opName in opNames) { - getOp(opName)?.let { historicalOps.add(it) } - } - - val discreteAccessList = mutableListOf<DiscreteAccess>() - historicalOps.forEach { - for (i in 0 until it.discreteAccessCount) { - val opEntry: AttributedOpEntry = it.getDiscreteAccessAt(i) - discreteAccessList.add( - DiscreteAccess( - it.opName, - opEntry.getLastAccessTime(DISCRETE_ACCESS_OP_FLAGS), - opEntry.getLastDuration(DISCRETE_ACCESS_OP_FLAGS), - opEntry.getLastProxyInfo(DISCRETE_ACCESS_OP_FLAGS) - ) - ) - } - } - - if (discreteAccessList.isEmpty()) { - return null - } - return discreteAccessList.sortedWith(compareBy { -it.accessTimeMs }) - } - - /** - * Retrieves all discrete accesses for the provided op names, if any. - * - * Returns null if there are no accesses. - */ - private fun AttributedHistoricalOps.getDiscreteAccesses( - opNames: List<String> - ): List<DiscreteAccess>? { - if (opCount == 0) { - return null - } - - val historicalOps = mutableListOf<HistoricalOp>() - for (opName in opNames) { - getOp(opName)?.let { historicalOps.add(it) } - } - - val discreteAccessList = mutableListOf<DiscreteAccess>() - historicalOps.forEach { - for (i in 0 until it.discreteAccessCount) { - val attributedOpEntry: AttributedOpEntry = it.getDiscreteAccessAt(i) - discreteAccessList.add( - DiscreteAccess( - it.opName, - attributedOpEntry.getLastAccessTime(DISCRETE_ACCESS_OP_FLAGS), - attributedOpEntry.getLastDuration(DISCRETE_ACCESS_OP_FLAGS), - attributedOpEntry.getLastProxyInfo(DISCRETE_ACCESS_OP_FLAGS) - ) - ) - } - } - - if (discreteAccessList.isEmpty()) { - return null - } - return discreteAccessList.sortedWith(compareBy { -it.accessTimeMs }) - } - - private fun partitionOpsByPermission(ops: Set<String>): Map<String, List<String>> = - ops.groupBy { getPlatformPermissionGroupForOp(it) ?: NO_PERM_GROUP } - .filter { it.key != NO_PERM_GROUP } - } - - /** - * Data class representing permissions accesses for a particular permission group by a - * particular package and user. - */ - data class AppPermissionDiscreteAccesses( - val appPermissionId: AppPermissionId, - val discreteAccesses: List<DiscreteAccess> - ) - - /** - * Data class representing permissions accesses for a particular permission group by a - * particular package and user, partitioned by attribution tag. - */ - data class AttributedAppPermissionDiscreteAccesses( - val appPermissionId: AppPermissionId, - val attributedDiscreteAccesses: Map<String, List<DiscreteAccess>> - ) - - /** Data class representing a discrete permission access. */ - data class DiscreteAccess( - val opName: String, - val accessTimeMs: Long, - val accessDurationMs: Long, - val proxy: OpEventProxyInfo? - ) -} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/LightPackageOps.kt b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/LightPackageOps.kt deleted file mode 100644 index b65fda5ea..000000000 --- a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/LightPackageOps.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.permissioncontroller.permission.model.livedatatypes.v31 - -import android.app.AppOpsManager.OP_FLAG_SELF -import android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED -import android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY -import android.app.AppOpsManager.PackageOps -import android.os.UserHandle -import com.android.permissioncontroller.permission.utils.PermissionMapping.getPlatformPermissionGroupForOp - -/** - * Light version of [PackageOps] class, tracking the last permission access for system permission - * groups. - */ -data class LightPackageOps( - /** Name of the package. */ - val packageName: String, - /** [UserHandle] running the package. */ - val userHandle: UserHandle, - /** - * Mapping of permission group name to the last access time of any op backing a permission in - * the group. - */ - val lastPermissionGroupAccessTimesMs: Map<String, Long> -) { - constructor( - ops: Set<String>, - packageOps: PackageOps - ) : this( - packageOps.packageName, - UserHandle.getUserHandleForUid(packageOps.uid), - createLastPermissionGroupAccessTimesMap(ops, packageOps) - ) - - /** Companion object for [LightPackageOps]. */ - companion object { - /** Flags to use for querying an op's last access time. */ - private const val OPS_LAST_ACCESS_FLAGS = - OP_FLAG_SELF or OP_FLAG_TRUSTED_PROXIED or OP_FLAG_TRUSTED_PROXY - - /** Creates a mapping from permission group to the last time it was accessed. */ - private fun createLastPermissionGroupAccessTimesMap( - opNames: Set<String>, - packageOps: PackageOps - ): Map<String, Long> { - val lastAccessTimeMs = mutableMapOf<String, Long>() - // Add keys for all permissions groups covered by the provided ops, regardless of - // whether they have been observed recently. - for (permissionGroup in - opNames.mapNotNull { getPlatformPermissionGroupForOp(it) }.toSet()) { - lastAccessTimeMs[permissionGroup] = -1 - } - - for (opEntry in packageOps.ops) { - val permissionGroupOfOp = getPlatformPermissionGroupForOp(opEntry.opStr) ?: continue - lastAccessTimeMs[permissionGroupOfOp] = - maxOf( - lastAccessTimeMs[permissionGroupOfOp] ?: -1, - opEntry.getLastAccessTime(OPS_LAST_ACCESS_FLAGS) - ) - } - - return lastAccessTimeMs - } - } -} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/package-info.java b/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/package-info.java deleted file mode 100644 index b724ba43f..000000000 --- a/PermissionController/src/com/android/permissioncontroller/permission/model/livedatatypes/v31/package-info.java +++ /dev/null @@ -1,18 +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. - */ - -@androidx.annotation.RequiresApi(android.os.Build.VERSION_CODES.S) -package com.android.permissioncontroller.permission.model.livedatatypes.v31; diff --git a/PermissionController/src/com/android/permissioncontroller/role/Role.md b/PermissionController/src/com/android/permissioncontroller/role/Role.md index 255214495..8d995d557 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/Role.md +++ b/PermissionController/src/com/android/permissioncontroller/role/Role.md @@ -56,6 +56,13 @@ receive short text messages, photos, videos, and more". For default apps, this s the default app detail page as a footer. This attribute is required if the role is `visible`. - `exclusive`: Whether the role is exclusive. If a role is exclusive, at most one application is allowed to be its holder. +- `exclusivity`: Whether the role is exclusive and what type of exclusivity behavior it has. A role +can have exclusivity of `none`, `user`, or `profileGroup`. + - `none`: Role allows multiple holders + - `user`: Role allows at most one holder within each user + - `profileGroup`: (SDK 36+ only, fallsback to `user` on lower SDK) Role allows at most one holder +within a profile group (e.g. full user and work +profile) - `fallBackToDefaultHolder`: Whether the role should fall back to the default holder. This attribute is optional and defaults to `false`. - `featureFlag`: Optional feature flag for the role be available, as the fully qualified name of diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/utils/AdminRestrictedPermissionsUtilsTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/utils/AdminRestrictedPermissionsUtilsTest.kt index 2a60e1325..35543b6f1 100644 --- a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/utils/AdminRestrictedPermissionsUtilsTest.kt +++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/permission/utils/AdminRestrictedPermissionsUtilsTest.kt @@ -19,12 +19,14 @@ package com.android.permissioncontroller.tests.mocking.permission.utils import android.app.admin.DevicePolicyManager import android.content.Context import android.health.connect.HealthPermissions +import android.os.Build import android.permission.flags.Flags import android.platform.test.annotations.AsbSecurityTest import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress import com.android.modules.utils.build.SdkLevel import com.android.permissioncontroller.permission.utils.v31.AdminRestrictedPermissionsUtils import org.junit.Assert.assertEquals @@ -74,12 +76,12 @@ object AdminRestrictedPermissionsUtilsTest { companion object { /** - * Returns a list of arrays containing the following values: - * 0. Permission name (String) - * 1. Permission group name (String) - * 2. Can admin grant sensors permissions (Boolean) - * 3. Expected return from mayAdminGrantPermission method (Boolean) - */ + * Returns a list of arrays containing the following values: + * 0. Permission name (String) + * 1. Permission group name (String) + * 2. Can admin grant sensors permissions (Boolean) + * 3. Expected return from mayAdminGrantPermission method (Boolean) + */ @JvmStatic @Parameterized.Parameters(name = "{index}: validate({0}, {1}, {3}) = {4}") fun getParameters(): List<Array<out Any?>> { @@ -109,6 +111,7 @@ object AdminRestrictedPermissionsUtilsTest { class AdminRestrictedPermissionsUtilsSingleTest { @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.BAKLAVA) @RequiresFlagsEnabled(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED) fun addAdminRestrictedPermission_addsPermissionToRestrictedList() { var canGrant = diff --git a/tests/cts/permissionui/src/android/permissionui/cts/LocationAccuracyTest.kt b/tests/cts/permissionui/src/android/permissionui/cts/LocationAccuracyTest.kt index be4b82932..4781fb895 100644 --- a/tests/cts/permissionui/src/android/permissionui/cts/LocationAccuracyTest.kt +++ b/tests/cts/permissionui/src/android/permissionui/cts/LocationAccuracyTest.kt @@ -72,6 +72,8 @@ class LocationAccuracyTest : BaseUsePermissionTest() { } @Test + @Ignore("b/396478581") + // Ignore this test until the cause of flakiness is identified. fun testPrecisePermissionIsGranted() { installPackage(APP_APK_PATH_31) diff --git a/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt b/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt index 64db7d47a..f5d230deb 100644 --- a/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt +++ b/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt @@ -75,6 +75,7 @@ import com.android.safetycenter.internaldata.SafetyCenterIds import com.android.safetycenter.resources.SafetyCenterResourcesApk import com.android.safetycenter.testing.Coroutines.TIMEOUT_LONG import com.android.safetycenter.testing.Coroutines.TIMEOUT_SHORT +import com.android.safetycenter.testing.Coroutines.assertWithTimeout import com.android.safetycenter.testing.Coroutines.waitForWithTimeout import com.android.safetycenter.testing.SafetyCenterApisWithShellPermissions.dismissSafetyCenterIssueWithPermission import com.android.safetycenter.testing.SafetyCenterApisWithShellPermissions.getSafetyCenterConfigWithPermission @@ -2356,18 +2357,14 @@ class SafetyCenterManagerTest { groupId = MULTIPLE_SOURCES_GROUP_ID_2, ) ) - waitForWithTimeout(timeout = RESURFACE_TIMEOUT, checkPeriod = RESURFACE_CHECK) { - val hasResurfaced = - safetyCenterManager - .getSafetyCenterDataWithPermission() - .issues - .contains( - safetyCenterTestData.safetyCenterIssueCritical( - SOURCE_ID_5, - groupId = MULTIPLE_SOURCES_GROUP_ID_2, - ) + assertWithTimeout(timeout = RESURFACE_TIMEOUT, checkPeriod = RESURFACE_CHECK) { + assertThat(safetyCenterManager.getSafetyCenterDataWithPermission().issues) + .contains( + safetyCenterTestData.safetyCenterIssueCritical( + SOURCE_ID_5, + groupId = MULTIPLE_SOURCES_GROUP_ID_2, ) - hasResurfaced + ) } } @@ -3986,9 +3983,9 @@ class SafetyCenterManagerTest { companion object { private val RESURFACE_DELAY = Duration.ofMillis(500) - // Wait 3 times the RESURFACE_DELAY before asserting whether an issue has or has not + // Wait 5 times the RESURFACE_DELAY before asserting whether an issue has or has not // resurfaced. Use a constant additive error buffer if we increase the delay considerably. - private val RESURFACE_TIMEOUT = RESURFACE_DELAY.multipliedBy(3) + private val RESURFACE_TIMEOUT = RESURFACE_DELAY.multipliedBy(5) // Check more than once during a RESURFACE_DELAY before asserting whether an issue has or // has not resurfaced. Use a different check logic (focused at the expected resurface time) diff --git a/tests/hostside/safetycenter/helper-app/src/android/safetycenter/hostside/device/SafetyCenterNotificationLoggingHelperTests.kt b/tests/hostside/safetycenter/helper-app/src/android/safetycenter/hostside/device/SafetyCenterNotificationLoggingHelperTests.kt index 60e6e41ec..c56c913b6 100644 --- a/tests/hostside/safetycenter/helper-app/src/android/safetycenter/hostside/device/SafetyCenterNotificationLoggingHelperTests.kt +++ b/tests/hostside/safetycenter/helper-app/src/android/safetycenter/hostside/device/SafetyCenterNotificationLoggingHelperTests.kt @@ -69,6 +69,12 @@ class SafetyCenterNotificationLoggingHelperTests { @Test fun sendNotification() { safetyCenterTestHelper.setData(SINGLE_SOURCE_ID, newTestDataWithNotifiableIssue()) + TestNotificationListener.waitForSingleNotificationMatching( + NotificationCharacteristics( + actions = listOf("See issue"), + safetySourceId = SINGLE_SOURCE_ID, + ) + ) } @Test @@ -104,7 +110,7 @@ class SafetyCenterNotificationLoggingHelperTests { statusBarNotificationWithChannel.statusBarNotification.notification.contentIntent SafetyCenterActivityLauncher.executeBlockAndExit( launchActivity = { PendingIntentSender.send(contentIntent) }, - block = {} // No action required + block = {}, // No action required ) } } diff --git a/tests/utils/safetycenter/java/com/android/safetycenter/testing/Coroutines.kt b/tests/utils/safetycenter/java/com/android/safetycenter/testing/Coroutines.kt index cc8c53d5e..47f5165e2 100644 --- a/tests/utils/safetycenter/java/com/android/safetycenter/testing/Coroutines.kt +++ b/tests/utils/safetycenter/java/com/android/safetycenter/testing/Coroutines.kt @@ -22,6 +22,7 @@ import java.time.Duration import kotlinx.coroutines.DEBUG_PROPERTY_NAME import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_AUTO import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout @@ -68,6 +69,20 @@ object Coroutines { runBlockingWithTimeout(timeout) { waitFor(checkPeriod, condition) } } + /** Check an assertion passes, with a timeout if it does not. */ + fun assertWithTimeout( + timeout: Duration = TIMEOUT_LONG, + checkPeriod: Duration = CHECK_PERIOD, + assertion: () -> Unit, + ) { + try { + runBlockingWithTimeout(timeout) { assertThatWaiting(checkPeriod, assertion) } + } catch (ex: TimeoutCancellationException) { + // Rerun the assertion to generate a meaningful error message that isn't just "timeout" + assertion() + } + } + /** Retries a [fallibleAction] until no errors are thrown or a timeout occurs. */ fun waitForSuccessWithTimeout( timeout: Duration = TIMEOUT_LONG, @@ -105,6 +120,21 @@ object Coroutines { } } + /** Check an assertion passes using coroutines. */ + private suspend fun assertThatWaiting( + checkPeriod: Duration = CHECK_PERIOD, + assertion: () -> Unit, + ) { + while (true) { + try { + assertion() + break + } catch (ex: AssertionError) { + delay(checkPeriod.toMillis()) + } + } + } + private const val TAG: String = "Coroutines" /** A medium period, to be used for conditions that are expected to change. */ |