Snap for 11189729 from f9eab98e3bd7174b125d997f0aafbbb547efdeca to studio-iguana-release

Change-Id: Icbb602962c77deed36697bae633b0494e621096a
diff --git a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceHandle.kt b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceHandle.kt
index 939db18..dd138dc 100644
--- a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceHandle.kt
+++ b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceHandle.kt
@@ -16,6 +16,7 @@
 package com.google.gct.directaccess.provisioner
 
 import com.android.adblib.ConnectedDevice
+import com.android.adblib.deviceInfo
 import com.android.adblib.deviceProperties
 import com.android.adblib.serialNumber
 import com.android.sdklib.deviceprovisioner.ActivationAction
@@ -120,6 +121,8 @@
   private var hasUserForceCheckedInDevice = false
   /** Tracks [Reservation] activated */
   private var hasReservationActivated = false
+  /** Tracks reservation expired shown */
+  private var hasShownReservationExpiredNotification = false
   /** [ContentManagerListener] that listens to panel changes in RDW */
   private var rdwPanelChangeListener: ContentManagerListener? = null
 
@@ -138,7 +141,7 @@
           return@invokeOnCompletion
         }
         if (hasReservationActivated && state.connectedDevice != null) {
-          notificationManager.showReservationExpiredNotification()
+          showReservationExpiredNotification()
         }
 
         when (sessionState) {
@@ -202,6 +205,13 @@
       }
     }
 
+  private fun showReservationExpiredNotification() =
+    synchronized(this) {
+      if (hasShownReservationExpiredNotification) return@synchronized
+      notificationManager.showReservationExpiredNotification()
+      hasShownReservationExpiredNotification = true
+    }
+
   override val activationAction =
     object : ActivationAction {
       /** Starts connection to the remote device. */
@@ -418,6 +428,9 @@
               state.reservation?.endTime?.epochSecond
             )
           }
+          if (reservationFlow.value.sessionState.isClosed()) {
+            showReservationExpiredNotification()
+          }
         }
         trackDisconnectMetric(true, connectionStateReason.toFailureReason(throwable))
       }
diff --git a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessNotificationManager.kt b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessNotificationManager.kt
index fc4b31c..0fa2b4a 100644
--- a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessNotificationManager.kt
+++ b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessNotificationManager.kt
@@ -166,7 +166,7 @@
     if (reservationExpiringNotification.notificationVisible) reservationExpiringNotification.show()
   }
 
-  fun showReservationExpiredNotification() {
+  fun showReservationExpiredNotification() =
     stickyNotificationGroup
       .createNotification(
         "$deviceName session ended",
@@ -183,7 +183,6 @@
         }
       )
       .notify(project)
-  }
 
   private fun getDeviceDisconnectedNotificationPhrase(reservationExpireTime: Long): String? {
     val timeRemaining =
diff --git a/directaccess/testSrc/com/google/gct/directaccess/analytics/DirectAccessUsageTrackerTest.kt b/directaccess/testSrc/com/google/gct/directaccess/analytics/DirectAccessUsageTrackerTest.kt
index beac760..8ff24cb 100644
--- a/directaccess/testSrc/com/google/gct/directaccess/analytics/DirectAccessUsageTrackerTest.kt
+++ b/directaccess/testSrc/com/google/gct/directaccess/analytics/DirectAccessUsageTrackerTest.kt
@@ -45,6 +45,7 @@
 import com.google.gct.directaccess.provisioner.DirectAccessDeviceProvisionerPlugin
 import com.google.gct.directaccess.provisioner.DirectAccessDeviceTemplate
 import com.google.gct.directaccess.provisioner.PLUGIN_ID
+import com.google.gct.directaccess.rule.CleanUpNotificationRule
 import com.google.gct.login.GoogleLogin
 import com.google.gct.login.LoginState
 import com.google.gct.login.LoginStateRule
@@ -98,9 +99,14 @@
   private val projectRule = ProjectRule()
   private val grpcConnectionRule = GrpcConnectionRule(listOf(service))
   private val loginStateRule = LoginStateRule(LoginStatus.LoggedIn("test@gmail.com"))
+  private val cleanUpNotificationRule = CleanUpNotificationRule(projectRule)
 
   @get:Rule
-  val ruleChain = RuleChain.outerRule(projectRule).around(grpcConnectionRule).around(loginStateRule)
+  val ruleChain: RuleChain =
+    RuleChain.outerRule(projectRule)
+      .around(grpcConnectionRule)
+      .around(loginStateRule)
+      .around(cleanUpNotificationRule)
 
   private val session = FakeAdbSession()
   private lateinit var plugin: DirectAccessDeviceProvisionerPlugin
@@ -488,6 +494,11 @@
       }
       yieldUntil { reservationFlow.value.sessionState.isClosed() }
 
+      // Wait for the end reservation event.
+      // This also ensures that the sticky notification for reservation end is shown
+      // This notification is then cleaned by the rule
+      findUsageEvent(END_RESERVATION)
+
       val studioEvent = findUsageEvent(DISCONNECT_DEVICE)
       assertThat(studioEvent.kind).isEqualTo(AndroidStudioEvent.EventKind.DIRECT_ACCESS_USAGE_EVENT)
 
diff --git a/directaccess/testSrc/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerTest.kt b/directaccess/testSrc/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerTest.kt
index e421542..d4cbe16 100644
--- a/directaccess/testSrc/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerTest.kt
+++ b/directaccess/testSrc/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerTest.kt
@@ -19,6 +19,7 @@
 import com.android.adblib.DevicePropertyNames
 import com.android.adblib.DeviceSelector
 import com.android.adblib.DeviceState
+import com.android.adblib.connectedDevicesTracker
 import com.android.adblib.scope
 import com.android.adblib.testing.FakeAdbSession
 import com.android.adblib.testingutils.CoroutineTestUtils.runBlockingWithTimeout
@@ -53,6 +54,7 @@
 import com.google.gct.directaccess.TestUtils.getNotifications
 import com.google.gct.directaccess.TestUtils.refreshReservations
 import com.google.gct.directaccess.TestUtils.reservation
+import com.google.gct.directaccess.rule.CleanUpNotificationRule
 import com.google.gct.directaccess.rule.FakeToolWindowRule
 import com.google.gct.directaccess.ui.SelectDeviceDialog
 import com.google.gct.login.LoginStateRule
@@ -96,7 +98,6 @@
 import kotlinx.coroutines.withContext
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.RuleChain
@@ -112,6 +113,7 @@
   private val grpcConnectionRule = GrpcConnectionRule(listOf(service))
   private val loginStateRule = LoginStateRule(LoginStatus.LoggedIn("test@gmail.com"))
   private val fakeToolWindowRule = FakeToolWindowRule(projectRule)
+  private val cleanUpNotificationRule = CleanUpNotificationRule(projectRule)
 
   @get:Rule
   val ruleChain: RuleChain =
@@ -119,6 +121,7 @@
       .around(grpcConnectionRule)
       .around(loginStateRule)
       .around(fakeToolWindowRule)
+      .around(cleanUpNotificationRule)
 
   private val session = FakeAdbSession()
   private lateinit var plugin: DirectAccessDeviceProvisionerPlugin
@@ -483,7 +486,7 @@
     // Device removed after logout.
     loginStateRule.state.value = LoginStatus.LoggedOut
     yieldUntil {
-      provisioner.templates.value.all { !it.activationAction.presentation.value.enabled }
+      provisioner.templates.value.all { (it as DirectAccessDeviceTemplate).activeDevice == null }
     }
     yieldUntil { provisioner.devices.value.isEmpty() }
 
@@ -517,7 +520,6 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testActionsInNotificationOnDisconnectDevice() = runBlockingWithTimeout {
     val template = plugin.templates.value[0]
 
@@ -560,7 +562,6 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testActionsInNotificationOnExpiringReservation() = runBlockingWithTimeout {
     val deviceInfo = deviceInfoListProvider()[0]
     val template = plugin.templates.value[0]
@@ -608,7 +609,6 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testBannerNotificationForReservationExpiringNotification() = runBlockingWithTimeout {
     val bannerNotifications = mutableListOf<EditorNotificationPanel>()
     val handle = setupReservationExpiringTest()
@@ -646,7 +646,6 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testBalloonNotificationForReservationExpiringNotification() = runBlockingWithTimeout {
     val bannerNotifications = mutableListOf<EditorNotificationPanel>()
     val handle = setupReservationExpiringTest()
@@ -670,7 +669,6 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testNotificationOnUnexpectedDeviceDisconnection() = runBlockingWithTimeout {
     val template = plugin.templates.value[0]
     template.activationAction.activate()
@@ -692,22 +690,18 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testNotificationExpiringOnDisconnectDevice() = runBlockingWithTimeout {
     val template = plugin.templates.value[0]
-    template.activationAction.activate()
+    val handle = template.activationAction.activate() as DirectAccessDeviceHandle
     yieldUntil { provisioner.devices.value.isNotEmpty() }
-    val handle = (template as DirectAccessDeviceTemplate).activeDevice
-    handle?.reservation?.let {
-      directAccessReservationManager.fetchReservationFlow(it.name).waitUntilActive()
-    }
+    directAccessReservationManager.fetchReservationFlow(handle.reservation.name).waitUntilActive()
 
-    handle?.deactivationAction?.deactivate()
-    yieldUntil { handle?.connectionState is ConnectionState.Disconnected }
+    handle.deactivationAction.deactivate()
+    yieldUntil { handle.connectionState is ConnectionState.Disconnected }
 
     val firstNotificationsList = getNotifications(projectRule.project)
     assertThat(firstNotificationsList.size).isEqualTo(1)
-    handle?.activationAction?.activate()
+    handle.activationAction.activate()
 
     yieldUntil { firstNotificationsList[0].isExpired }
     // Expiring a notification does not guarantee it is no longer visible. Wait for the notification
@@ -716,12 +710,12 @@
 
     // Device will reconnect after previous action. Disconnect again to show notification for force
     // check-in
-    handle?.deactivationAction?.deactivate()
+    handle.deactivationAction.deactivate()
 
     val secondNotificationsList = getNotifications(projectRule.project)
     assertThat(secondNotificationsList.size).isEqualTo(1)
 
-    handle?.reservationAction?.endReservation()
+    handle.reservationAction.endReservation()
     yieldUntil { getNotifications(projectRule.project).isEmpty() }
   }
 
@@ -784,7 +778,6 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testNoNotificationWhenReservationCancelledBeforeActive() = runBlockingWithTimeout {
     val template = plugin.templates.value[0] as DirectAccessDeviceTemplate
 
@@ -815,12 +808,12 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testNoNotificationOnForceCheckIn() = runBlockingWithTimeout {
     setupConnection { reservationName ->
       object : FakeDirectAccessConnection(directAccessReservationManager, reservationName, scope) {
         override suspend fun endReservation(withGracePeriod: Boolean) {
           session.hostServices.disconnect(deviceAddress()!!)
+          yieldUntil { session.connectedDevicesTracker.connectedDevices.value.isEmpty() }
           super.endReservation(withGracePeriod)
         }
       }
@@ -842,7 +835,6 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testNoNotificationOnForceCheckInWhenReservationEndDelayed() = runBlockingWithTimeout {
     setupConnection { reservationName ->
       object :
@@ -962,8 +954,20 @@
   }
 
   @Test
-  @Ignore("b/309136739")
   fun testStickyNotificationOnReservationExpiry() = runBlockingWithTimeout {
+    setupConnection { reservationName ->
+      object :
+        FakeDirectAccessConnection(
+          directAccessReservationManager,
+          reservationName,
+          scope.createChildScope(true)
+        ) {
+        override suspend fun closeConnection(stateReason: DirectAccessConnection.StateReason) {
+          session.hostServices.disconnect(deviceAddress()!!)
+          super.closeConnection(stateReason)
+        }
+      }
+    }
     val deviceInfo = deviceInfoListProvider()[0]
     val template = plugin.templates.value[0] as DirectAccessDeviceTemplate
 
@@ -986,12 +990,14 @@
     session.hostServices.devices =
       DeviceList(listOf(com.android.adblib.DeviceInfo(serialNumber, DeviceState.ONLINE)), listOf())
     yieldUntil { handle.state is Connected }
+    yieldUntil { provisioner.devices.value.isNotEmpty() }
 
     directAccessReservationManager.cancelReservation(flow.value.name)
 
     yieldUntil { !flow.value.isActive() }
-    assertThat(template.activeDevice).isNull()
+    yieldUntil { template.activeDevice == null }
 
+    yieldUntil { getNotifications(projectRule.project).isNotEmpty() }
     val notificationsList = getNotifications(projectRule.project)
     assertThat(notificationsList.size).isEqualTo(1)
 
diff --git a/directaccess/testSrc/com/google/gct/directaccess/rule/CleanUpNotificationRule.kt b/directaccess/testSrc/com/google/gct/directaccess/rule/CleanUpNotificationRule.kt
new file mode 100644
index 0000000..167be8d
--- /dev/null
+++ b/directaccess/testSrc/com/google/gct/directaccess/rule/CleanUpNotificationRule.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.google.gct.directaccess.rule
+
+import com.google.gct.directaccess.TestUtils.getNotifications
+import com.intellij.testFramework.ProjectRule
+import org.junit.rules.ExternalResource
+
+internal class CleanUpNotificationRule(private val projectRule: ProjectRule) : ExternalResource() {
+  override fun after() {
+    getNotifications(projectRule.project).forEach { it.expire() }
+  }
+}
diff --git a/test-recorder/src/com/google/gct/testrecorder/ui/RecordingDialog.java b/test-recorder/src/com/google/gct/testrecorder/ui/RecordingDialog.java
index 0b3ec61..329adb9 100644
--- a/test-recorder/src/com/google/gct/testrecorder/ui/RecordingDialog.java
+++ b/test-recorder/src/com/google/gct/testrecorder/ui/RecordingDialog.java
@@ -1017,8 +1017,8 @@
         helper.addDependency(ANDROID_TEST_IMPLEMENTATION,
                              compactNotation,
                              excludes,
-                             new ExactDependencyMatcher(compactNotation),
-                             gradleBuildModel);
+                             gradleBuildModel,
+                             new ExactDependencyMatcher(ANDROID_TEST_IMPLEMENTATION, compactNotation));
       }
     }.queue();
   }
diff --git a/test-recorder/src/com/google/gct/testrecorder/ui/TestRecorderAction.java b/test-recorder/src/com/google/gct/testrecorder/ui/TestRecorderAction.java
index b55bf2e..c71875b 100644
--- a/test-recorder/src/com/google/gct/testrecorder/ui/TestRecorderAction.java
+++ b/test-recorder/src/com/google/gct/testrecorder/ui/TestRecorderAction.java
@@ -16,10 +16,8 @@
 package com.google.gct.testrecorder.ui;
 
 import com.android.annotations.VisibleForTesting;
-import com.android.ide.common.repository.GradleCoordinate;
 import com.android.tools.analytics.UsageTracker;
 import com.android.tools.analytics.UsageTrackerUtils;
-import com.android.tools.idea.projectsystem.AndroidModuleSystem;
 import com.android.tools.idea.projectsystem.ProjectSystemUtil;
 import com.android.tools.idea.run.deployment.DeviceAndSnapshotComboBoxTargetProvider;
 import com.google.common.collect.Lists;
@@ -41,7 +39,6 @@
 import com.intellij.openapi.actionSystem.AnAction;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.Presentation;
-import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.DumbService;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.ui.Messages;
@@ -99,7 +96,12 @@
     }
 
     // Disable Espresso Test Recorder for Compose projects, since Espresso Testing Framework does not support Compose.
-    TestRecorderRunConfigurationProxy testRecorderConfigurationProxy = TestRecorderRunConfigurationProxy.getInstance(getSuitableRunConfigurations(project).get(0));
+    List<RunConfiguration> runConfigurations = getSuitableRunConfigurations(project);
+    if (runConfigurations.isEmpty()) {
+      presentation.setEnabled(false);
+      return;
+    }
+    TestRecorderRunConfigurationProxy testRecorderConfigurationProxy = TestRecorderRunConfigurationProxy.getInstance(runConfigurations.get(0));
     if (ProjectSystemUtil.getModuleSystem(testRecorderConfigurationProxy.getModule()).getUsesCompose()) {
       presentation.setEnabled(false);
       return;