diff options
Diffstat (limited to 'tests')
19 files changed, 551 insertions, 25 deletions
diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt index 8a241de32a2b..209a14b3657d 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt @@ -17,6 +17,7 @@ package com.android.server.wm.flicker.service import android.app.Instrumentation +import android.platform.test.rule.DisableNotificationCooldownSettingRule import android.platform.test.rule.NavigationModeRule import android.platform.test.rule.PressHomeRule import android.platform.test.rule.UnlockScreenRule @@ -48,6 +49,7 @@ object Utils { clearCacheAfterParsing = false ) ) + .around(DisableNotificationCooldownSettingRule()) .around(PressHomeRule()) } } diff --git a/tests/FlickerTests/IME/Android.bp b/tests/FlickerTests/IME/Android.bp index 3538949cbc8d..ccc3683f0b93 100644 --- a/tests/FlickerTests/IME/Android.bp +++ b/tests/FlickerTests/IME/Android.bp @@ -39,6 +39,10 @@ android_test { defaults: ["FlickerTestsDefault"], manifest: "AndroidManifest.xml", test_config_template: "AndroidTestTemplate.xml", + test_suites: [ + "device-tests", + "device-platinum-tests", + ], srcs: ["src/**/*"], static_libs: ["FlickerTestsBase"], data: ["trace_config/*"], diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt index dc5013519dbf..ed6e8df3e293 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt @@ -23,6 +23,7 @@ import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.traces.component.ComponentNameMatcher +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import org.junit.FixMethodOrder @@ -77,6 +78,7 @@ class CloseImeShownOnAppStartToAppOnPressBackTest(flicker: LegacyFlickerTest) : @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible() + @FlakyTest(bugId = 330486656) @Presubmit @Test fun imeAppLayerIsAlwaysVisible() { diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt index c29e71ce4c79..07fc2300286a 100644 --- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt +++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt @@ -18,6 +18,7 @@ package com.android.server.wm.flicker.notification import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit +import android.platform.test.rule.DisableNotificationCooldownSettingRule import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.FlickerTestData @@ -37,6 +38,7 @@ import com.android.server.wm.flicker.navBarWindowIsVisibleAtEnd import com.android.server.wm.flicker.taskBarLayerIsVisibleAtEnd import com.android.server.wm.flicker.taskBarWindowIsVisibleAtEnd import org.junit.Assume +import org.junit.ClassRule import org.junit.FixMethodOrder import org.junit.Ignore import org.junit.Test @@ -208,5 +210,10 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams() = LegacyFlickerTestFactory.nonRotationTests() + + /** Ensures that posted notifications will alert and HUN even just after boot. */ + @ClassRule + @JvmField + val disablenotificationCooldown = DisableNotificationCooldownSettingRule() } } diff --git a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt index c7da778b752b..c49b509a9db3 100644 --- a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt +++ b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/RotationTransition.kt @@ -21,6 +21,7 @@ import android.tools.device.apphelpers.StandardAppHelper import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.subject.layers.LayerTraceEntrySubject +import android.tools.flicker.subject.layers.LayersTraceSubject import android.tools.traces.component.ComponentNameMatcher import android.tools.traces.component.IComponentMatcher import android.tools.traces.surfaceflinger.Display @@ -46,6 +47,7 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : BaseTest(flicker flicker.assertLayers { this.visibleLayersShownMoreThanOneConsecutiveEntry( ignoreLayers = + LayersTraceSubject.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS + listOf( ComponentNameMatcher.SPLASH_SCREEN, ComponentNameMatcher.SNAPSHOT, diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp index a85d809257cd..f367c38b06e9 100644 --- a/tests/Input/Android.bp +++ b/tests/Input/Android.bp @@ -18,25 +18,31 @@ android_test { "src/**/*.java", "src/**/*.kt", ], + asset_dirs: ["assets"], kotlincflags: [ "-Werror", ], platform_apis: true, certificate: "platform", static_libs: [ + "android.view.flags-aconfig-java", "androidx.test.core", "androidx.test.ext.junit", "androidx.test.ext.truth", "androidx.test.rules", "androidx.test.runner", "androidx.test.uiautomator_uiautomator", + "collector-device-lib", "compatibility-device-util-axt", + "cts-input-lib", + "cts-wm-util", "flag-junit", "frameworks-base-testutils", "hamcrest-library", "kotlin-test", "mockito-target-minus-junit4", "platform-test-annotations", + "platform-screenshot-diff-core", "services.core.unboosted", "servicestests-utils", "testables", diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml index 3b723ddf811f..a05d08ccceba 100644 --- a/tests/Input/AndroidManifest.xml +++ b/tests/Input/AndroidManifest.xml @@ -22,6 +22,8 @@ <uses-permission android:name="android.permission.MONITOR_INPUT"/> <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> <application android:label="InputTest" android:debuggable="true"> diff --git a/tests/Input/AndroidTest.xml b/tests/Input/AndroidTest.xml index f602c5124e77..8db37058af2b 100644 --- a/tests/Input/AndroidTest.xml +++ b/tests/Input/AndroidTest.xml @@ -28,4 +28,10 @@ <!-- Take screenshot upon test failure --> <option name="screenshot-on-failure" value="true" /> </object> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="pull-pattern-keys" value="input_.*" /> + <!-- Pull files created by tests, like the output of screenshot tests --> + <option name="directory-keys" value="/storage/emulated/0/InputTests" /> + <option name="collect-on-run-ended-only" value="false" /> + </metrics_collector> </configuration> diff --git a/tests/Input/assets/testPointerFillStyle.png b/tests/Input/assets/testPointerFillStyle.png Binary files differnew file mode 100644 index 000000000000..b2354f8f4799 --- /dev/null +++ b/tests/Input/assets/testPointerFillStyle.png diff --git a/tests/Input/src/com/android/test/input/AnrTest.kt b/tests/Input/src/com/android/test/input/AnrTest.kt index 4893d14ad79b..8d1fc508ffe7 100644 --- a/tests/Input/src/com/android/test/input/AnrTest.kt +++ b/tests/Input/src/com/android/test/input/AnrTest.kt @@ -21,26 +21,33 @@ import androidx.test.filters.MediumTest import android.app.ActivityManager import android.app.ApplicationExitInfo +import android.content.Context import android.graphics.Rect +import android.hardware.display.DisplayManager import android.os.Build import android.os.IInputConstants.UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS import android.os.SystemClock import android.provider.Settings import android.provider.Settings.Global.HIDE_ERROR_DIALOGS +import android.server.wm.CtsWindowInfoUtils.waitForStableWindowGeometry import android.testing.PollingCheck -import android.view.InputDevice -import android.view.MotionEvent import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until +import com.android.cts.input.DebugInputRule +import com.android.cts.input.UinputTouchScreen + +import java.util.concurrent.TimeUnit + import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -69,6 +76,9 @@ class AnrTest { private val DISPATCHING_TIMEOUT = (UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS * Build.HW_TIMEOUT_MULTIPLIER) + @get:Rule + val debugInputRule = DebugInputRule() + @Before fun setUp() { val contentResolver = instrumentation.targetContext.contentResolver @@ -84,12 +94,14 @@ class AnrTest { } @Test + @DebugInputRule.DebugInput(bug = 339924248) fun testGestureMonitorAnr_Close() { triggerAnr() clickCloseAppOnAnrDialog() } @Test + @DebugInputRule.DebugInput(bug = 339924248) fun testGestureMonitorAnr_Wait() { triggerAnr() clickWaitOnAnrDialog() @@ -105,7 +117,7 @@ class AnrTest { val closeAppButton: UiObject2? = uiDevice.wait(Until.findObject(By.res("android:id/aerr_close")), 20000) if (closeAppButton == null) { - fail("Could not find anr dialog") + fail("Could not find anr dialog/close button") return } closeAppButton.click() @@ -150,6 +162,18 @@ class AnrTest { assertEquals(ApplicationExitInfo.REASON_ANR, reasons[0].reason) } + private fun clickOnObject(obj: UiObject2) { + val displayManager = + instrumentation.context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + val display = displayManager.getDisplay(obj.getDisplayId()) + val touchScreen = UinputTouchScreen(instrumentation, display) + + val rect: Rect = obj.visibleBounds + val pointer = touchScreen.touchDown(rect.centerX(), rect.centerY()) + pointer.lift() + touchScreen.close() + } + private fun triggerAnr() { startUnresponsiveActivity() val uiDevice: UiDevice = UiDevice.getInstance(instrumentation) @@ -160,13 +184,7 @@ class AnrTest { return } - val rect: Rect = obj.visibleBounds - val downTime = SystemClock.uptimeMillis() - val downEvent = MotionEvent.obtain(downTime, downTime, - MotionEvent.ACTION_DOWN, rect.left.toFloat(), rect.top.toFloat(), 0 /* metaState */) - downEvent.source = InputDevice.SOURCE_TOUCHSCREEN - - instrumentation.uiAutomation.injectInputEvent(downEvent, false /* sync*/) + clickOnObject(obj) SystemClock.sleep(DISPATCHING_TIMEOUT.toLong()) // default ANR timeout for gesture monitors } @@ -175,5 +193,6 @@ class AnrTest { val flags = " -W -n " val startCmd = "am start $flags $PACKAGE_NAME/.UnresponsiveGestureMonitorActivity" instrumentation.uiAutomation.executeShellCommand(startCmd) + waitForStableWindowGeometry(5L, TimeUnit.SECONDS) } } diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt new file mode 100644 index 000000000000..dac425329f48 --- /dev/null +++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.input + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.os.Environment +import android.view.ContextThemeWrapper +import android.view.PointerIcon +import android.view.flags.Flags.enableVectorCursorA11ySettings +import android.view.flags.Flags.enableVectorCursors +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestName +import org.junit.runner.RunWith +import platform.test.screenshot.GoldenPathManager +import platform.test.screenshot.PathConfig +import platform.test.screenshot.ScreenshotTestRule +import platform.test.screenshot.assertAgainstGolden +import platform.test.screenshot.matchers.BitmapMatcher +import platform.test.screenshot.matchers.PixelPerfectMatcher + +/** + * Unit tests for PointerIcon. + * + * Run with: + * atest InputTests:com.android.test.input.PointerIconLoadingTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class PointerIconLoadingTest { + private lateinit var context: Context + private lateinit var exactScreenshotMatcher: BitmapMatcher + + @get:Rule + val testName = TestName() + + @get:Rule + val screenshotRule = ScreenshotTestRule(GoldenPathManager( + InstrumentationRegistry.getInstrumentation().getContext(), + ASSETS_PATH, + TEST_OUTPUT_PATH, + PathConfig() + ), disableIconPool = false) + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + val config = + Configuration(context.resources.configuration).apply { + densityDpi = DENSITY_DPI + screenWidthDp = SCREEN_WIDTH_DP + screenHeightDp = SCREEN_HEIGHT_DP + smallestScreenWidthDp = SCREEN_WIDTH_DP + } + context = context.createConfigurationContext(config) + + exactScreenshotMatcher = PixelPerfectMatcher() + } + + @Test + fun testPointerFillStyle() { + assumeTrue(enableVectorCursors()) + assumeTrue(enableVectorCursorA11ySettings()) + + val theme: Resources.Theme = context.getResources().newTheme() + theme.setTo(context.getTheme()) + theme.applyStyle( + PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_GREEN), + /* force= */ true) + + val pointerIcon = + PointerIcon.getLoadedSystemIcon( + ContextThemeWrapper(context, theme), + PointerIcon.TYPE_ARROW, + /* useLargeIcons= */ false) + + pointerIcon.getBitmap().assertAgainstGolden( + screenshotRule, + testName.methodName, + exactScreenshotMatcher + ) + } + + companion object { + const val DENSITY_DPI = 160 + const val SCREEN_WIDTH_DP = 480 + const val SCREEN_HEIGHT_DP = 800 + const val ASSETS_PATH = "tests/input/assets" + val TEST_OUTPUT_PATH = Environment.getExternalStorageDirectory().absolutePath + + "/InputTests/" + + PointerIconLoadingTest::class.java.simpleName + } +} diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java index 5cdfb2858b33..5a27593c7a36 100644 --- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java @@ -393,5 +393,10 @@ public class LegacyProtoLogImplTest { this.mLogToLogcat = logToLogcat; } + @Override + public int getId() { + return ordinal(); + } + } } diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java index f6ac080ebf73..1d7b6b348e10 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java @@ -725,5 +725,10 @@ public class PerfettoProtoLogImplTest { this.mLogToLogcat = logToLogcat; } + @Override + public int getId() { + return ordinal(); + } + } } diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java index 4267c2c127ae..60456f9ea10f 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java @@ -174,5 +174,10 @@ public class ProtoLogImplTest { this.mLogToLogcat = logToLogcat; } + @Override + public int getId() { + return ordinal(); + } + } } diff --git a/tests/TrustTests/AndroidManifest.xml b/tests/TrustTests/AndroidManifest.xml index 30cf345db34d..2f6c0dd14adc 100644 --- a/tests/TrustTests/AndroidManifest.xml +++ b/tests/TrustTests/AndroidManifest.xml @@ -78,6 +78,7 @@ <action android:name="android.service.trust.TrustAgentService" /> </intent-filter> </service> + <service android:name=".IsActiveUnlockRunningTrustAgent" android:exported="true" @@ -88,6 +89,16 @@ </intent-filter> </service> + <service + android:name=".UnlockAttemptTrustAgent" + android:exported="true" + android:label="Test Agent" + android:permission="android.permission.BIND_TRUST_AGENT"> + <intent-filter> + <action android:name="android.service.trust.TrustAgentService" /> + </intent-filter> + </service> + </application> <!-- self-instrumenting test package. --> diff --git a/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt new file mode 100644 index 000000000000..2c9361df63fd --- /dev/null +++ b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.trust.test + +import android.app.trust.TrustManager +import android.content.Context +import android.trust.BaseTrustAgentService +import android.trust.TrustTestActivity +import android.trust.test.lib.LockStateTrackingRule +import android.trust.test.lib.ScreenLockRule +import android.trust.test.lib.TestTrustListener +import android.trust.test.lib.TrustAgentRule +import android.util.Log +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +/** + * Test for the impacts of reporting unlock attempts. + * + * atest TrustTests:UnlockAttemptTest + */ +@RunWith(AndroidJUnit4::class) +class UnlockAttemptTest { + private val context = getApplicationContext<Context>() + private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager + private val userId = context.userId + private val activityScenarioRule = ActivityScenarioRule(TrustTestActivity::class.java) + private val screenLockRule = ScreenLockRule(requireStrongAuth = true) + private val lockStateTrackingRule = LockStateTrackingRule() + private val trustAgentRule = + TrustAgentRule<UnlockAttemptTrustAgent>(startUnlocked = false, startEnabled = false) + + private val trustListener = UnlockAttemptTrustListener() + private val agent get() = trustAgentRule.agent + + @get:Rule + val rule: RuleChain = + RuleChain.outerRule(activityScenarioRule) + .around(screenLockRule) + .around(lockStateTrackingRule) + .around(trustAgentRule) + + @Before + fun setUp() { + trustManager.registerTrustListener(trustListener) + } + + @Test + fun successfulUnlockAttempt_allowsTrustAgentToStart() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) { + trustAgentRule.enableTrustAgent() + + triggerSuccessfulUnlock() + + trustAgentRule.verifyAgentIsRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START) + } + + @Test + fun successfulUnlockAttempt_notifiesTrustAgent() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldSuccessfulCount = agent.successfulUnlockCallCount + val oldFailedCount = agent.failedUnlockCallCount + + triggerSuccessfulUnlock() + + assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount + 1) + assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount) + } + + @Test + fun successfulUnlockAttempt_notifiesTrustListenerOfManagedTrust() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0 + + triggerSuccessfulUnlock() + + assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo( + oldTrustManagedChangedCount + 1 + ) + } + + @Test + fun failedUnlockAttempt_doesNotAllowTrustAgentToStart() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) { + trustAgentRule.enableTrustAgent() + + triggerFailedUnlock() + + trustAgentRule.ensureAgentIsNotRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START) + } + + @Test + fun failedUnlockAttempt_notifiesTrustAgent() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldSuccessfulCount = agent.successfulUnlockCallCount + val oldFailedCount = agent.failedUnlockCallCount + + triggerFailedUnlock() + + assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount) + assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount + 1) + } + + @Test + fun failedUnlockAttempt_doesNotNotifyTrustListenerOfManagedTrust() = + runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) { + val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0 + + triggerFailedUnlock() + + assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo( + oldTrustManagedChangedCount + ) + } + + private fun runUnlockAttemptTest( + enableAndVerifyTrustAgent: Boolean, + managingTrust: Boolean, + testBlock: () -> Unit, + ) { + if (enableAndVerifyTrustAgent) { + Log.i(TAG, "Triggering successful unlock") + triggerSuccessfulUnlock() + Log.i(TAG, "Enabling and waiting for trust agent") + trustAgentRule.enableAndVerifyTrustAgentIsRunning( + MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START + ) + Log.i(TAG, "Managing trust: $managingTrust") + agent.setManagingTrust(managingTrust) + await() + } + testBlock() + } + + private fun triggerSuccessfulUnlock() { + screenLockRule.successfulScreenLockAttempt() + trustAgentRule.reportSuccessfulUnlock() + await() + } + + private fun triggerFailedUnlock() { + screenLockRule.failedScreenLockAttempt() + trustAgentRule.reportFailedUnlock() + await() + } + + companion object { + private const val TAG = "UnlockAttemptTest" + private fun await(millis: Long = 500) = Thread.sleep(millis) + private const val MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START = 10000L + } +} + +class UnlockAttemptTrustAgent : BaseTrustAgentService() { + var successfulUnlockCallCount: Long = 0 + private set + var failedUnlockCallCount: Long = 0 + private set + + override fun onUnlockAttempt(successful: Boolean) { + super.onUnlockAttempt(successful) + if (successful) { + successfulUnlockCallCount++ + } else { + failedUnlockCallCount++ + } + } +} + +private class UnlockAttemptTrustListener : TestTrustListener() { + var enabledTrustAgentsChangedCount = mutableMapOf<Int, Int>() + var onTrustManagedChangedCount = mutableMapOf<Int, Int>() + + override fun onEnabledTrustAgentsChanged(userId: Int) { + enabledTrustAgentsChangedCount.compute(userId) { _: Int, curr: Int? -> + if (curr == null) 0 else curr + 1 + } + } + + data class TrustChangedParams( + val enabled: Boolean, + val newlyUnlocked: Boolean, + val userId: Int, + val flags: Int, + val trustGrantedMessages: MutableList<String>? + ) + + val onTrustChangedCalls = mutableListOf<TrustChangedParams>() + + override fun onTrustChanged( + enabled: Boolean, + newlyUnlocked: Boolean, + userId: Int, + flags: Int, + trustGrantedMessages: MutableList<String> + ) { + onTrustChangedCalls += TrustChangedParams( + enabled, newlyUnlocked, userId, flags, trustGrantedMessages + ) + } + + override fun onTrustManagedChanged(enabled: Boolean, userId: Int) { + onTrustManagedChangedCount.compute(userId) { _: Int, curr: Int? -> + if (curr == null) 0 else curr + 1 + } + } +} diff --git a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt index f1edca3ff86e..1ccdcc623c5b 100644 --- a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt +++ b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt @@ -24,6 +24,8 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN import com.android.internal.widget.LockscreenCredential import com.google.common.truth.Truth.assertWithMessage import org.junit.rules.TestRule @@ -32,13 +34,18 @@ import org.junit.runners.model.Statement /** * Sets a screen lock on the device for the duration of the test. + * + * @param requireStrongAuth Whether a strong auth is required at the beginning. + * If true, trust agents will not be available until the user verifies their credentials. */ -class ScreenLockRule : TestRule { +class ScreenLockRule(val requireStrongAuth: Boolean = false) : TestRule { private val context: Context = getApplicationContext() + private val userId = context.userId private val uiDevice = UiDevice.getInstance(getInstrumentation()) private val windowManager = checkNotNull(WindowManagerGlobal.getWindowManagerService()) private val lockPatternUtils = LockPatternUtils(context) private var instantLockSavedValue = false + private var strongAuthSavedValue: Int = 0 override fun apply(base: Statement, description: Description) = object : Statement() { override fun evaluate() { @@ -46,10 +53,12 @@ class ScreenLockRule : TestRule { dismissKeyguard() setScreenLock() setLockOnPowerButton() + configureStrongAuthState() try { base.evaluate() } finally { + restoreStrongAuthState() removeScreenLock() revertLockOnPowerButton() dismissKeyguard() @@ -57,6 +66,22 @@ class ScreenLockRule : TestRule { } } + private fun configureStrongAuthState() { + strongAuthSavedValue = lockPatternUtils.getStrongAuthForUser(userId) + if (requireStrongAuth) { + Log.d(TAG, "Triggering strong auth due to simulated lockdown") + lockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, userId) + wait("strong auth required after lockdown") { + lockPatternUtils.getStrongAuthForUser(userId) == + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN + } + } + } + + private fun restoreStrongAuthState() { + lockPatternUtils.requireStrongAuth(strongAuthSavedValue, userId) + } + private fun verifyNoScreenLockAlreadySet() { assertWithMessage("Screen Lock must not already be set on device") .that(lockPatternUtils.isSecure(context.userId)) @@ -82,6 +107,22 @@ class ScreenLockRule : TestRule { } } + fun successfulScreenLockAttempt() { + lockPatternUtils.verifyCredential(LockscreenCredential.createPin(PIN), context.userId, 0) + lockPatternUtils.userPresent(context.userId) + wait("strong auth not required") { + lockPatternUtils.getStrongAuthForUser(context.userId) == STRONG_AUTH_NOT_REQUIRED + } + } + + fun failedScreenLockAttempt() { + lockPatternUtils.verifyCredential( + LockscreenCredential.createPin(WRONG_PIN), + context.userId, + 0 + ) + } + private fun setScreenLock() { lockPatternUtils.setLockCredential( LockscreenCredential.createPin(PIN), @@ -121,5 +162,6 @@ class ScreenLockRule : TestRule { companion object { private const val TAG = "ScreenLockRule" private const val PIN = "0000" + private const val WRONG_PIN = "0001" } } diff --git a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt index 18bc029b6845..404c6d968b3a 100644 --- a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt +++ b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt @@ -20,14 +20,15 @@ import android.app.trust.TrustManager import android.content.ComponentName import android.content.Context import android.trust.BaseTrustAgentService +import android.trust.test.lib.TrustAgentRule.Companion.invoke import android.util.Log import androidx.test.core.app.ApplicationProvider.getApplicationContext import com.android.internal.widget.LockPatternUtils import com.google.common.truth.Truth.assertWithMessage +import kotlin.reflect.KClass import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import kotlin.reflect.KClass /** * Enables a trust agent and causes the system service to bind to it. @@ -37,7 +38,9 @@ import kotlin.reflect.KClass * @constructor Creates the rule. Do not use; instead, use [invoke]. */ class TrustAgentRule<T : BaseTrustAgentService>( - private val serviceClass: KClass<T> + private val serviceClass: KClass<T>, + private val startUnlocked: Boolean, + private val startEnabled: Boolean, ) : TestRule { private val context: Context = getApplicationContext() private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager @@ -48,11 +51,18 @@ class TrustAgentRule<T : BaseTrustAgentService>( override fun apply(base: Statement, description: Description) = object : Statement() { override fun evaluate() { verifyTrustServiceRunning() - unlockDeviceWithCredential() - enableTrustAgent() + if (startUnlocked) { + reportSuccessfulUnlock() + } else { + Log.i(TAG, "Trust manager not starting in unlocked state") + } try { - verifyAgentIsRunning() + if (startEnabled) { + enableAndVerifyTrustAgentIsRunning() + } else { + Log.i(TAG, "Trust agent ${serviceClass.simpleName} not enabled") + } base.evaluate() } finally { disableTrustAgent() @@ -64,12 +74,22 @@ class TrustAgentRule<T : BaseTrustAgentService>( assertWithMessage("Trust service is not running").that(trustManager).isNotNull() } - private fun unlockDeviceWithCredential() { - Log.d(TAG, "Unlocking device with credential") + fun reportSuccessfulUnlock() { + Log.i(TAG, "Reporting successful unlock") trustManager.reportUnlockAttempt(true, context.userId) } - private fun enableTrustAgent() { + fun reportFailedUnlock() { + Log.i(TAG, "Reporting failed unlock") + trustManager.reportUnlockAttempt(false, context.userId) + } + + fun enableAndVerifyTrustAgentIsRunning(maxWait: Long = 30000L) { + enableTrustAgent() + verifyAgentIsRunning(maxWait) + } + + fun enableTrustAgent() { val componentName = ComponentName(context, serviceClass.java) val userId = context.userId Log.i(TAG, "Enabling trust agent ${componentName.flattenToString()} for user $userId") @@ -79,12 +99,18 @@ class TrustAgentRule<T : BaseTrustAgentService>( lockPatternUtils.setEnabledTrustAgents(agents, userId) } - private fun verifyAgentIsRunning() { - wait("${serviceClass.simpleName} to be running") { + fun verifyAgentIsRunning(maxWait: Long = 30000L) { + wait("${serviceClass.simpleName} to be running", maxWait) { BaseTrustAgentService.instance(serviceClass) != null } } + fun ensureAgentIsNotRunning(window: Long = 30000L) { + ensure("${serviceClass.simpleName} is not running", window) { + BaseTrustAgentService.instance(serviceClass) == null + } + } + private fun disableTrustAgent() { val componentName = ComponentName(context, serviceClass.java) val userId = context.userId @@ -97,13 +123,23 @@ class TrustAgentRule<T : BaseTrustAgentService>( companion object { /** - * Creates a new rule for the specified agent class. Example usage: + * Creates a new rule for the specified agent class. Starts with the device unlocked and + * the trust agent enabled. Example usage: * ``` * @get:Rule val rule = TrustAgentRule<MyTestAgent>() * ``` + * + * Also supports setting different device lock and trust agent enablement states: + * ``` + * @get:Rule val rule = TrustAgentRule<MyTestAgent>(startUnlocked = false, startEnabled = false) + * ``` */ - inline operator fun <reified T : BaseTrustAgentService> invoke() = - TrustAgentRule(T::class) + inline operator fun <reified T : BaseTrustAgentService> invoke( + startUnlocked: Boolean = true, + startEnabled: Boolean = true, + ) = + TrustAgentRule(T::class, startUnlocked, startEnabled) + private const val TAG = "TrustAgentRule" } diff --git a/tests/TrustTests/src/android/trust/test/lib/utils.kt b/tests/TrustTests/src/android/trust/test/lib/Utils.kt index e047202f6740..3b32b47a6160 100644 --- a/tests/TrustTests/src/android/trust/test/lib/utils.kt +++ b/tests/TrustTests/src/android/trust/test/lib/Utils.kt @@ -39,7 +39,7 @@ internal fun wait( ) { var waited = 0L var count = 0 - while (!conditionFunction.invoke(count)) { + while (!conditionFunction(count)) { assertWithMessage("Condition exceeded maximum wait time of $maxWait ms: $description") .that(waited <= maxWait) .isTrue() @@ -49,3 +49,34 @@ internal fun wait( Thread.sleep(rate) } } + +/** + * Ensures that [conditionFunction] is true with a failed assertion if it is not within [window] + * ms. + * + * The condition function can perform additional logic (for example, logging or attempting to make + * the condition become true). + * + * @param conditionFunction function which takes the attempt count & returns whether the condition + * is met + */ +internal fun ensure( + description: String? = null, + window: Long = 30000L, + rate: Long = 50L, + conditionFunction: (count: Int) -> Boolean +) { + var waited = 0L + var count = 0 + while (waited <= window) { + assertWithMessage("Condition failed within $window ms: $description").that( + conditionFunction( + count + ) + ).isTrue() + waited += rate + count++ + Log.i(TAG, "Ensuring $description ($waited/$window) #$count") + Thread.sleep(rate) + } +} |