Snap for 11390309 from f7494d85793577e897fa052d3dae17a704af9c29 to studio-jellyfish-release

Change-Id: Ife7aa40b3058b29e2b4f2ed284822aa1995c0a36
diff --git a/directaccess/src/com/google/gct/directaccess/provisioner/DeviceInfo.kt b/directaccess/src/com/google/gct/directaccess/provisioner/DeviceInfo.kt
index b0c1852..a047a61 100644
--- a/directaccess/src/com/google/gct/directaccess/provisioner/DeviceInfo.kt
+++ b/directaccess/src/com/google/gct/directaccess/provisioner/DeviceInfo.kt
@@ -34,7 +34,10 @@
   val screenY: Int,
   val screenDensity: Int,
   val deviceAvailabilityEstimateSeconds: Long?,
-)
+) {
+  /** A string key to distinguish itself from other [DeviceInfo]s. */
+  val key = "$id/$api"
+}
 
 data class DeviceSelection(var isSelected: Boolean, val deviceInfo: DeviceInfo)
 
diff --git a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceHandle.kt b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceHandle.kt
index 451fbc6..237db6c 100644
--- a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceHandle.kt
+++ b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceHandle.kt
@@ -146,8 +146,16 @@
         when (sessionState) {
           SessionState.EXPIRED -> trackEndReservation(true, EndReservationType.EXPIRE)
           SessionState.FINISHED -> trackEndReservation(true, EndReservationType.FORCE_CHECK_IN)
-          else ->
+          SessionState.ERROR ->
             trackEndReservation(false, EndReservationType.ERROR, FailureReason.UNKNOWN_FAILURE)
+          SessionState.UNAVAILABLE ->
+            trackEndReservation(
+              false,
+              EndReservationType.ERROR,
+              FailureReason.FAILED_TO_ALLOCATE_DEVICE,
+            )
+          else ->
+            trackEndReservation(false, EndReservationType.UNKNOWN, FailureReason.UNKNOWN_FAILURE)
         }
       }
   }
diff --git a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerPlugin.kt b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerPlugin.kt
index 30bc1df..016785e 100644
--- a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerPlugin.kt
+++ b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerPlugin.kt
@@ -88,15 +88,15 @@
             cloudProjectManager.accessibleDeviceInfoListFlow.stateFlow.collect {
               newAccessibleDeviceInfoList ->
               accessibleDeviceInfoMapFlow.value =
-                newAccessibleDeviceInfoList.groupBy { it.id }.mapValues { it.value.first() }
+                newAccessibleDeviceInfoList.groupBy { it.key }.mapValues { it.value.first() }
               project.service<DirectAccessService>().deviceSelectionListFlow.update {
                 oldDeviceSelectionList ->
                 val accessibleDeviceIdSet = newAccessibleDeviceInfoList.map { it.id }.toSet()
-                val selectedDeviceIdSet =
-                  oldDeviceSelectionList.filter { it.isSelected }.map { it.deviceInfo.id }.toSet()
+                val deselectedDeviceIdSet =
+                  oldDeviceSelectionList.filter { !it.isSelected }.map { it.deviceInfo.id }.toSet()
                 val accessibleDeviceSelectionList =
                   newAccessibleDeviceInfoList.map {
-                    DeviceSelection(it.id in selectedDeviceIdSet, it)
+                    DeviceSelection(it.id !in deselectedDeviceIdSet, it)
                   }
                 val inaccessibleDeviceSelectionList =
                   oldDeviceSelectionList.filter { it.deviceInfo.id !in accessibleDeviceIdSet }
@@ -129,11 +129,11 @@
               .map { selection -> selection.deviceInfo }
               .map { deviceInfo ->
                 existingDeviceInfoMap[deviceInfo]?.firstOrNull()
-                  ?: cachedTemplatesMap.computeIfAbsent(deviceInfo.id) {
+                  ?: cachedTemplatesMap.computeIfAbsent(deviceInfo.key) {
                     val templateScope = scope.createChildScope(isSupervisor = true)
                     val deviceInfoFlow =
                       accessibleDeviceInfoMapFlow
-                        .mapNotNull { deviceMap -> deviceMap[deviceInfo.id] }
+                        .mapNotNull { deviceMap -> deviceMap[deviceInfo.key] }
                         .stateIn(templateScope, SharingStarted.Eagerly, deviceInfo)
                     DirectAccessDeviceTemplate(
                       project,
@@ -142,8 +142,8 @@
                       templateScope,
                       reservationsFlow.combine(accessibleDeviceInfoMapFlow) {
                         reservations,
-                        deviceInfoSet ->
-                        reservations != null && deviceInfoSet[deviceInfo.id] != null
+                        deviceInfoMap ->
+                        reservations != null && deviceInfoMap[deviceInfo.key] != null
                       },
                     )
                   }
diff --git a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceTemplate.kt b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceTemplate.kt
index a421807..8df85aa 100644
--- a/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceTemplate.kt
+++ b/directaccess/src/com/google/gct/directaccess/provisioner/DirectAccessDeviceTemplate.kt
@@ -66,7 +66,7 @@
   val deviceInfo: DeviceInfo
     get() = deviceInfoFlow.value
 
-  override val id = DeviceId(PLUGIN_ID, true, "model_id=${deviceInfo.id}")
+  override val id = DeviceId(PLUGIN_ID, true, "model_id=${deviceInfo.key}")
 
   override val properties = deviceInfo.toDeviceProperties()
 
diff --git a/directaccess/testSrc/com/google/gct/directaccess/TestUtils.kt b/directaccess/testSrc/com/google/gct/directaccess/TestUtils.kt
index c32ad21..eac97a5 100644
--- a/directaccess/testSrc/com/google/gct/directaccess/TestUtils.kt
+++ b/directaccess/testSrc/com/google/gct/directaccess/TestUtils.kt
@@ -87,6 +87,19 @@
         150,
         30,
       ),
+      DeviceInfo(
+        "id4",
+        "Google",
+        "Pixel Watch",
+        "Google",
+        "watch",
+        34,
+        DeviceType.WEAR_OS,
+        50,
+        100,
+        150,
+        30,
+      ),
     )
   }
 
diff --git a/directaccess/testSrc/com/google/gct/directaccess/analytics/DirectAccessUsageTrackerTest.kt b/directaccess/testSrc/com/google/gct/directaccess/analytics/DirectAccessUsageTrackerTest.kt
index 66a9ba1..f276bc2 100644
--- a/directaccess/testSrc/com/google/gct/directaccess/analytics/DirectAccessUsageTrackerTest.kt
+++ b/directaccess/testSrc/com/google/gct/directaccess/analytics/DirectAccessUsageTrackerTest.kt
@@ -73,6 +73,7 @@
 import com.google.wireless.android.sdk.stats.DirectAccessUsageEvent.ExtendReservationDetails.ExtendReservationDuration.SIXTY_MINUTES
 import com.google.wireless.android.sdk.stats.DirectAccessUsageEvent.ExtendReservationDetails.ExtendReservationDuration.THIRTY_MINUTES
 import com.google.wireless.android.sdk.stats.DirectAccessUsageEvent.FailureReason
+import com.google.wireless.android.sdk.stats.DirectAccessUsageEvent.FailureReason.FAILED_TO_ALLOCATE_DEVICE
 import com.google.wireless.android.sdk.stats.DirectAccessUsageEvent.FailureReason.UNKNOWN_FAILURE
 import com.intellij.openapi.application.ApplicationManager
 import com.intellij.openapi.components.service
@@ -763,6 +764,37 @@
   }
 
   @Test
+  fun testEndReservationFailMetricWhenDeviceNotAllocated() = runBlockingWithTimeout {
+    val template = plugin.templates.value[0] as DirectAccessDeviceTemplate
+
+    // Activate device
+    val handle = template.activationAction.activate() as DirectAccessDeviceHandle
+    yieldUntil {
+      template.activeDevice?.connection?.state?.value?.connection is
+        DirectAccessConnection.ConnectionState.Connected
+    }
+    val reservationFlow =
+      directAccessReservationManager.fetchReservationFlow(handle.reservation.name)
+    reservationFlow.waitUntilActive()
+    (reservationFlow as MutableStateFlow).update {
+      it.toBuilder().apply { sessionState = Reservation.SessionState.UNAVAILABLE }.build()
+    }
+    yieldUntil { reservationFlow.value.sessionState == Reservation.SessionState.UNAVAILABLE }
+
+    val studioEvent = findUsageEvent(END_RESERVATION)
+    assertThat(studioEvent.kind).isEqualTo(AndroidStudioEvent.EventKind.DIRECT_ACCESS_USAGE_EVENT)
+
+    val directAccessEvent = studioEvent.directAccessUsageEvent
+    assertThat(directAccessEvent.type).isEqualTo(END_RESERVATION)
+    assertThat(directAccessEvent.hasDeviceSessionId()).isTrue()
+    assertThat(directAccessEvent.failureReason).isEqualTo(FAILED_TO_ALLOCATE_DEVICE)
+
+    val endReservationDetails = directAccessEvent.endReservationDetails
+    assertThat(endReservationDetails.success).isFalse()
+    assertThat(endReservationDetails.endReservationType).isEqualTo(ERROR)
+  }
+
+  @Test
   fun trackEndReservationFailMetricWhenErrorEndingReservation() = runBlockingWithTimeout {
     // Override default connection setup
     setupConnection { reservationName ->
diff --git a/directaccess/testSrc/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerTest.kt b/directaccess/testSrc/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerTest.kt
index 5aba87e..70ea646 100644
--- a/directaccess/testSrc/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerTest.kt
+++ b/directaccess/testSrc/com/google/gct/directaccess/provisioner/DirectAccessDeviceProvisionerTest.kt
@@ -161,9 +161,9 @@
     plugin = DirectAccessDeviceProvisionerPlugin(session.scope, projectRule.project)
     provisioner = DeviceProvisioner.create(session, listOf(plugin), testDeviceIcons)
     projectRule.project.showAllTemplates { selectionList ->
-      // Templates are not added to the provisioner automatically after switching to a cloud project
+      // Templates are all added to the provisioner automatically after switching to a cloud project
       // with access to more devices.
-      selectionList.forEach { assertThat(it.isSelected).isFalse() }
+      selectionList.forEach { assertThat(it.isSelected).isTrue() }
     }
     yieldUntil { provisioner.templates.value.isNotEmpty() }
   }
@@ -252,7 +252,7 @@
     provisioner.templates.value[0].id.apply {
       assertThat(isTemplate).isTrue()
       assertThat(pluginId).isEqualTo(PLUGIN_ID)
-      assertThat(identifier).isEqualTo("model_id=id1")
+      assertThat(identifier).isEqualTo("model_id=id1/31")
     }
     assertThat(provisioner.templates.value[0].properties.title).isEqualTo("Google Pixel 5")
     assertThat(provisioner.templates.value[0].properties.resolution).isEqualTo(Resolution(100, 200))
@@ -271,6 +271,10 @@
     assertThat(provisioner.templates.value[3].properties.resolution).isEqualTo(Resolution(50, 100))
     assertThat(provisioner.templates.value[3].properties.density).isEqualTo(150)
     assertThat(provisioner.templates.value[3].properties.isRemote).isTrue()
+    assertThat(provisioner.templates.value[4].properties.title).isEqualTo("Google Pixel Watch")
+    assertThat(provisioner.templates.value[4].properties.resolution).isEqualTo(Resolution(50, 100))
+    assertThat(provisioner.templates.value[4].properties.density).isEqualTo(150)
+    assertThat(provisioner.templates.value[4].properties.isRemote).isTrue()
 
     // Log out
     loginStateRule.state.value = LoginStatus.LoggedOut
@@ -478,7 +482,7 @@
         FakeDirectAccessConnection(
           directAccessReservationManager,
           reservationName,
-          scope.createChildScope(true)
+          scope.createChildScope(true),
         ) {
         private val connectionScope = scope.createChildScope(isSupervisor = true)
 
@@ -964,7 +968,7 @@
   @RunsInEdt
   @Test
   fun selectTemplates() = runBlockingWithTimeout {
-    assertThat(plugin.templates.value.size).isEqualTo(4)
+    assertThat(plugin.templates.value.size).isEqualTo(5)
     val deviceInfoList =
       plugin.templates.value.map { (it as DirectAccessDeviceTemplate).deviceInfo }
 
@@ -973,7 +977,7 @@
         scope.launch { plugin.createDeviceTemplateAction.create() }
       }) {
         val dialog = it as SelectDeviceDialog
-        assertThat(dialog.deviceTable.componentCount).isEqualTo(4)
+        assertThat(dialog.deviceTable.componentCount).isEqualTo(5)
         val icons = dialog.deviceTable.findAllDescendants<JLabel>().mapNotNull { it.icon }.toList()
         assertThat(icons)
           .containsExactly(
@@ -981,6 +985,7 @@
             FIREBASE_DEVICE_PHONE,
             FIREBASE_DEVICE_PHONE,
             FIREBASE_DEVICE_WEAR,
+            FIREBASE_DEVICE_WEAR,
           )
         val checkboxList = dialog.deviceTable.findAllDescendants<JBCheckBox>().toList()
         checkboxList.forEach { assertThat(it.isSelected).isTrue() }
@@ -990,7 +995,7 @@
         dialog.clickDefaultButton()
       }
     }
-    yieldUntil { plugin.templates.value.size == 2 }
+    yieldUntil { plugin.templates.value.size == 3 }
     var templates = plugin.templates.value
     assertThat((templates[0] as DirectAccessDeviceTemplate).deviceInfo).isEqualTo(deviceInfoList[0])
     assertThat((templates[1] as DirectAccessDeviceTemplate).deviceInfo).isEqualTo(deviceInfoList[3])
@@ -1001,13 +1006,13 @@
         scope.launch { plugin.createDeviceTemplateAction.create() }
       }) {
         val dialog = it as SelectDeviceDialog
-        assertThat(dialog.deviceTable.componentCount).isEqualTo(4)
+        assertThat(dialog.deviceTable.componentCount).isEqualTo(5)
         val checkboxList = dialog.deviceTable.findAllDescendants<JBCheckBox>().toList()
         checkboxList[1].isSelected = true
         dialog.clickDefaultButton()
       }
     }
-    yieldUntil { plugin.templates.value.size == 3 }
+    yieldUntil { plugin.templates.value.size == 4 }
     templates = plugin.templates.value
     assertThat((templates[0] as DirectAccessDeviceTemplate).deviceInfo).isEqualTo(deviceInfoList[0])
     assertThat((templates[1] as DirectAccessDeviceTemplate).deviceInfo).isEqualTo(deviceInfoList[1])
@@ -1017,30 +1022,30 @@
   @RunsInEdt
   @Test
   fun selectTemplatesAfterLogout() = runBlockingWithTimeout {
-    assertThat(plugin.templates.value.size).isEqualTo(4)
+    assertThat(plugin.templates.value.size).isEqualTo(5)
     // Same templates after logout.
     loginStateRule.state.value = LoginStatus.LoggedOut
     yieldUntil {
       provisioner.templates.value.all { !it.activationAction.presentation.value.enabled }
     }
-    assertThat(plugin.templates.value.size).isEqualTo(4)
+    assertThat(plugin.templates.value.size).isEqualTo(5)
 
     // De-select a template.
     withContext(AndroidDispatchers.uiThread) {
       val dialog = SelectDeviceDialog(projectRule.project)
       createModalDialogAndInteractWithIt({ dialog.show() }) {
-        assertThat(dialog.deviceTable.componentCount).isEqualTo(4)
+        assertThat(dialog.deviceTable.componentCount).isEqualTo(5)
         val checkboxList = dialog.deviceTable.findAllDescendants<JBCheckBox>().toList()
         checkboxList[1].isSelected = false
         dialog.clickDefaultButton()
       }
     }
-    yieldUntil { plugin.templates.value.size == 3 }
+    yieldUntil { plugin.templates.value.size == 4 }
     // The de-selected device info gets removed from the table.
     withContext(AndroidDispatchers.uiThread) {
       val dialog = SelectDeviceDialog(projectRule.project)
       createModalDialogAndInteractWithIt({ dialog.show() }) {
-        assertThat(dialog.deviceTable.componentCount).isEqualTo(3)
+        assertThat(dialog.deviceTable.componentCount).isEqualTo(4)
         dialog.clickDefaultButton()
       }
     }
@@ -1049,16 +1054,16 @@
   @RunsInEdt
   @Test
   fun testSelectDeviceDialogSearchTest() = runBlockingWithTimeout {
-    assertThat(plugin.templates.value.size).isEqualTo(4)
+    assertThat(plugin.templates.value.size).isEqualTo(5)
 
     withContext(AndroidDispatchers.uiThread) {
       val dialog = SelectDeviceDialog(projectRule.project)
       createModalDialogAndInteractWithIt({ dialog.show() }) {
-        assertThat(dialog.deviceTable.componentCount).isEqualTo(4)
+        assertThat(dialog.deviceTable.componentCount).isEqualTo(5)
         val searchTextField = dialog.contentPanel.findAllDescendants<SearchTextField>().first()
         // Case-insensitive search
         searchTextField.text = "GoOgLe       WaTcH"
-        assertThat(dialog.deviceTable.componentCount).isEqualTo(1)
+        assertThat(dialog.deviceTable.componentCount).isEqualTo(2)
         assertThat(dialog.deviceTable.values[0].deviceInfo.name).isEqualTo("Pixel Watch")
 
         // Search for devices with 6 in their name