Handle non-activity trampolines from widgets
If a widget triggers an activity through a broadcast or service, we
currently do not trigger auth. This change adds detection logic to
detect when an activity is started within 1 second of a widget
interaction, and prompts for auth.
Bug: 350468769
Test: atest WidgetTrampolineInteractorTest
Test: atest WidgetInteractionHandlerTest
Flag: com.android.systemui.communal_widget_trampoline_fix
Change-Id: Ib8436a17cf6d413fb59a8d6cc6081b2d9660a3d1
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index cdbac33..9ab9ad7 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -998,6 +998,16 @@
}
flag {
+ name: "communal_widget_trampoline_fix"
+ namespace: "systemui"
+ description: "fixes activity starts caused by non-activity trampolines from widgets."
+ bug: "350468769"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "app_clips_backlinks"
namespace: "systemui"
description: "Enables Backlinks improvement feature in App Clips"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorTest.kt
new file mode 100644
index 0000000..b3ffc71
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorTest.kt
@@ -0,0 +1,220 @@
+/*
+ * 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 com.android.systemui.communal.domain.interactor
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.usage.UsageEvents
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.usagestats.data.repository.fakeUsageStatsRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.shared.system.taskStackChangeListeners
+import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WidgetTrampolineInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val activityStarter = kosmos.activityStarter
+ private val usageStatsRepository = kosmos.fakeUsageStatsRepository
+ private val taskStackChangeListeners = kosmos.taskStackChangeListeners
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val userTracker = kosmos.fakeUserTracker
+ private val systemClock = kosmos.fakeSystemClock
+
+ private val underTest = kosmos.widgetTrampolineInteractor
+
+ @Before
+ fun setUp() {
+ userTracker.set(listOf(MAIN_USER), 0)
+ systemClock.setCurrentTimeMillis(testScope.currentTime)
+ }
+
+ @Test
+ fun testNewTaskStartsWhileOnHub_triggersUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ moveTaskToFront()
+
+ verify(activityStarter).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testNewTaskStartsAfterExitingHub_doesNotTriggerUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ transition(from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN)
+ moveTaskToFront()
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testNewTaskStartsAfterTimeout_doesNotTriggerUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ advanceTime(2.seconds)
+ moveTaskToFront()
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testActivityResumedWhileOnHub_triggersUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+ advanceTime(1.seconds)
+
+ verify(activityStarter).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testActivityResumedAfterExitingHub_doesNotTriggerUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ transition(from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN)
+ addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+ advanceTime(1.seconds)
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testActivityDestroyed_doesNotTriggerUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ addActivityEvent(UsageEvents.Event.ACTIVITY_DESTROYED)
+ advanceTime(1.seconds)
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testMultipleActivityEvents_triggersUnlockOnlyOnce() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+ advanceTime(10.milliseconds)
+ addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+ advanceTime(1.seconds)
+
+ verify(activityStarter, times(1)).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ private fun TestScope.advanceTime(duration: Duration) {
+ systemClock.advanceTime(duration.inWholeMilliseconds)
+ advanceTimeBy(duration)
+ }
+
+ private fun TestScope.addActivityEvent(type: Int) {
+ usageStatsRepository.addEvent(
+ instanceId = 1,
+ user = MAIN_USER.userHandle,
+ packageName = "pkg.test",
+ timestamp = systemClock.currentTimeMillis(),
+ type = type,
+ )
+ runCurrent()
+ }
+
+ private fun TestScope.moveTaskToFront() {
+ taskStackChangeListeners.listenerImpl.onTaskMovedToFront(mock<RunningTaskInfo>())
+ runCurrent()
+ }
+
+ private suspend fun TestScope.transition(from: KeyguardState, to: KeyguardState) {
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(
+ from = from,
+ to = to,
+ value = 0.1f,
+ transitionState = TransitionState.STARTED,
+ ownerName = "test",
+ ),
+ TransitionStep(
+ from = from,
+ to = to,
+ value = 1f,
+ transitionState = TransitionState.FINISHED,
+ ownerName = "test",
+ ),
+ ),
+ testScope
+ )
+ runCurrent()
+ }
+
+ private companion object {
+ val MAIN_USER: UserInfo = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
index 023de52..400f736 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
@@ -27,7 +27,9 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.widgetTrampolineInteractor
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.ActivityStarter
@@ -67,9 +69,11 @@
with(kosmos) {
underTest =
WidgetInteractionHandler(
+ applicationScope = applicationCoroutineScope,
activityStarter = activityStarter,
communalSceneInteractor = communalSceneInteractor,
logBuffer = logcatLogBuffer(),
+ widgetTrampolineInteractor = widgetTrampolineInteractor,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractor.kt
new file mode 100644
index 0000000..7453368
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractor.kt
@@ -0,0 +1,140 @@
+/*
+ * 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 com.android.systemui.communal.domain.interactor
+
+import android.app.ActivityManager
+import com.android.systemui.common.usagestats.domain.UsageStatsInteractor
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
+import com.android.systemui.util.kotlin.race
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Detects activity starts that occur while the communal hub is showing, within a short delay of a
+ * widget interaction occurring. Used for detecting non-activity trampolines which otherwise would
+ * not prompt the user for authentication.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class WidgetTrampolineInteractor
+@Inject
+constructor(
+ private val activityStarter: ActivityStarter,
+ private val systemClock: SystemClock,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val taskStackChangeListeners: TaskStackChangeListeners,
+ private val usageStatsInteractor: UsageStatsInteractor,
+ @CommunalLog logBuffer: LogBuffer,
+) {
+ private companion object {
+ const val TAG = "WidgetTrampolineInteractor"
+ }
+
+ private val logger = Logger(logBuffer, TAG)
+
+ /** Waits for a new task to be moved to the foreground. */
+ private suspend fun waitForNewForegroundTask() = suspendCancellableCoroutine { cont ->
+ val listener =
+ object : TaskStackChangeListener {
+ override fun onTaskMovedToFront(taskInfo: ActivityManager.RunningTaskInfo) {
+ if (!cont.isCompleted) {
+ cont.resume(Unit, null)
+ }
+ }
+ }
+ taskStackChangeListeners.registerTaskStackListener(listener)
+ cont.invokeOnCancellation { taskStackChangeListeners.unregisterTaskStackListener(listener) }
+ }
+
+ /**
+ * Waits for an activity to enter a [ActivityEventModel.Lifecycle.RESUMED] state by periodically
+ * polling the system to see if any activities have started.
+ */
+ private suspend fun waitForActivityStartByPolling(startTime: Long): Boolean {
+ while (true) {
+ val events = usageStatsInteractor.queryActivityEvents(startTime = startTime)
+ if (events.any { event -> event.lifecycle == ActivityEventModel.Lifecycle.RESUMED }) {
+ return true
+ } else {
+ // Poll again in the future to check if an activity started.
+ delay(200.milliseconds)
+ }
+ }
+ }
+
+ /** Waits for a transition away from the hub to occur. */
+ private suspend fun waitForTransitionAwayFromHub() {
+ keyguardTransitionInteractor
+ .isFinishedIn(Scenes.Communal, KeyguardState.GLANCEABLE_HUB)
+ .takeWhile { it }
+ .collect {}
+ }
+
+ private suspend fun waitForActivityStartWhileOnHub(): Boolean {
+ val startTime = systemClock.currentTimeMillis()
+ return try {
+ return withTimeout(1.seconds) {
+ race(
+ {
+ waitForNewForegroundTask()
+ true
+ },
+ { waitForActivityStartByPolling(startTime) },
+ {
+ waitForTransitionAwayFromHub()
+ false
+ },
+ )
+ }
+ } catch (e: TimeoutCancellationException) {
+ false
+ }
+ }
+
+ /**
+ * Checks if an activity starts while on the glanceable hub and dismisses the keyguard if it
+ * does. This can detect activities started due to broadcast trampolines from widgets.
+ */
+ suspend fun waitForActivityStartAndDismissKeyguard() {
+ if (waitForActivityStartWhileOnHub()) {
+ logger.d("Detected trampoline, requesting unlock")
+ activityStarter.dismissKeyguardThenExecute(
+ /* action= */ { false },
+ /* cancel= */ null,
+ /* afterKeyguardGone= */ false
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
index c4edcac..99e3232 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
@@ -48,7 +48,17 @@
InteractionHandlerDelegate(
communalSceneInteractor,
findViewToAnimate = { view -> view is SmartspaceAppWidgetHostView },
- intentStarter = this::startIntent,
+ intentStarter =
+ object : InteractionHandlerDelegate.IntentStarter {
+ override fun startActivity(
+ intent: PendingIntent,
+ fillInIntent: Intent,
+ activityOptions: ActivityOptions,
+ controller: ActivityTransitionAnimator.Controller?
+ ): Boolean {
+ return startIntent(intent, fillInIntent, activityOptions, controller)
+ }
+ },
logger = Logger(logBuffer, TAG),
)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
index d2029d5..5e21afa 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
@@ -19,6 +19,7 @@
import android.app.ActivityOptions
import android.app.PendingIntent
import android.content.Intent
+import android.util.Pair as UtilPair
import android.view.View
import android.widget.RemoteViews
import androidx.core.util.component1
@@ -36,14 +37,28 @@
private val logger: Logger,
) : RemoteViews.InteractionHandler {
- /** Responsible for starting the pending intent for launching activities. */
- fun interface IntentStarter {
- fun startPendingIntent(
+ interface IntentStarter {
+ /** Responsible for starting the pending intent for launching activities. */
+ fun startActivity(
intent: PendingIntent,
fillInIntent: Intent,
activityOptions: ActivityOptions,
controller: ActivityTransitionAnimator.Controller?,
): Boolean
+
+ /** Responsible for starting the pending intent for non-activity launches. */
+ fun startPendingIntent(
+ view: View,
+ pendingIntent: PendingIntent,
+ fillInIntent: Intent,
+ activityOptions: ActivityOptions,
+ ): Boolean {
+ return RemoteViews.startPendingIntent(
+ view,
+ pendingIntent,
+ UtilPair(fillInIntent, activityOptions),
+ )
+ }
}
override fun onInteraction(
@@ -55,7 +70,7 @@
str1 = pendingIntent.toLoggingString()
str2 = pendingIntent.creatorPackage
}
- val launchOptions = response.getLaunchOptions(view)
+ val (fillInIntent, activityOptions) = response.getLaunchOptions(view)
return when {
pendingIntent.isActivity -> {
// Forward the fill-in intent and activity options retrieved from the response
@@ -67,15 +82,15 @@
communalSceneInteractor.setIsLaunchingWidget(true)
CommunalTransitionAnimatorController(it, communalSceneInteractor)
}
- val (fillInIntent, activityOptions) = launchOptions
- intentStarter.startPendingIntent(
+ intentStarter.startActivity(
pendingIntent,
fillInIntent,
activityOptions,
animationController
)
}
- else -> RemoteViews.startPendingIntent(view, pendingIntent, launchOptions)
+ else ->
+ intentStarter.startPendingIntent(view, pendingIntent, fillInIntent, activityOptions)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
index 0eeb506..121b4a3 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
@@ -21,22 +21,30 @@
import android.content.Intent
import android.view.View
import android.widget.RemoteViews
+import com.android.app.tracing.coroutines.launch
+import com.android.systemui.Flags.communalWidgetTrampolineFix
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.domain.interactor.WidgetTrampolineInteractor
import com.android.systemui.communal.util.InteractionHandlerDelegate
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
import com.android.systemui.plugins.ActivityStarter
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
@SysUISingleton
class WidgetInteractionHandler
@Inject
constructor(
+ @Application applicationScope: CoroutineScope,
private val activityStarter: ActivityStarter,
communalSceneInteractor: CommunalSceneInteractor,
+ private val widgetTrampolineInteractor: WidgetTrampolineInteractor,
@CommunalLog val logBuffer: LogBuffer,
) : RemoteViews.InteractionHandler {
@@ -48,7 +56,52 @@
InteractionHandlerDelegate(
communalSceneInteractor,
findViewToAnimate = { view -> view is CommunalAppWidgetHostView },
- intentStarter = this::startIntent,
+ intentStarter =
+ object : InteractionHandlerDelegate.IntentStarter {
+ private var job: Job? = null
+
+ override fun startActivity(
+ intent: PendingIntent,
+ fillInIntent: Intent,
+ activityOptions: ActivityOptions,
+ controller: ActivityTransitionAnimator.Controller?
+ ): Boolean {
+ cancelTrampolineMonitoring()
+ return startActivityIntent(
+ intent,
+ fillInIntent,
+ activityOptions,
+ controller
+ )
+ }
+
+ override fun startPendingIntent(
+ view: View,
+ pendingIntent: PendingIntent,
+ fillInIntent: Intent,
+ activityOptions: ActivityOptions
+ ): Boolean {
+ cancelTrampolineMonitoring()
+ if (communalWidgetTrampolineFix()) {
+ job =
+ applicationScope.launch("$TAG#monitorForActivityStart") {
+ widgetTrampolineInteractor
+ .waitForActivityStartAndDismissKeyguard()
+ }
+ }
+ return super.startPendingIntent(
+ view,
+ pendingIntent,
+ fillInIntent,
+ activityOptions
+ )
+ }
+
+ private fun cancelTrampolineMonitoring() {
+ job?.cancel()
+ job = null
+ }
+ },
logger = Logger(logBuffer, TAG),
)
@@ -58,7 +111,7 @@
response: RemoteViews.RemoteResponse
): Boolean = delegate.onInteraction(view, pendingIntent, response)
- private fun startIntent(
+ private fun startActivityIntent(
pendingIntent: PendingIntent,
fillInIntent: Intent,
extraOptions: ActivityOptions,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorKosmos.kt
new file mode 100644
index 0000000..8124224
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 com.android.systemui.communal.domain.interactor
+
+import com.android.systemui.common.usagestats.domain.interactor.usageStatsInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.shared.system.taskStackChangeListeners
+import com.android.systemui.util.time.fakeSystemClock
+
+val Kosmos.widgetTrampolineInteractor: WidgetTrampolineInteractor by
+ Kosmos.Fixture {
+ WidgetTrampolineInteractor(
+ activityStarter = activityStarter,
+ systemClock = fakeSystemClock,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
+ taskStackChangeListeners = taskStackChangeListeners,
+ usageStatsInteractor = usageStatsInteractor,
+ logBuffer = logcatLogBuffer("WidgetTrampolineInteractor"),
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/system/TaskStackChangeListenersKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/system/TaskStackChangeListenersKosmos.kt
new file mode 100644
index 0000000..67f611a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/system/TaskStackChangeListenersKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * 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 com.android.systemui.shared.system
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.taskStackChangeListeners: TaskStackChangeListeners by
+ Kosmos.Fixture { TaskStackChangeListeners.getTestInstance() }