Add "list" and "any_external" options to shade_display_override command

Those options allow to list the available displays (and see which one had the shade), and set the shade window on the first non-default display without caring about display ids.

+ Small refactor to use dagger in ShadePrimaryDisplayCommand and decouple it from ShadeDisplaysRepository

To use them "adb shell cmd statusbar shade_display_override list" or "any_external"

Bug: 362719719
Bug: 374264564
Test: ShadePrimaryDisplayCommandTest
Flag: com.android.systemui.shade_window_goes_around
Change-Id: Ib2be5359c2cd77e903bb7db592354d63cee17a09
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
similarity index 69%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
index 4e7839e..af01547 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
@@ -21,7 +21,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.display.data.repository.displayRepository
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.ShadePrimaryDisplayCommand
 import com.android.systemui.statusbar.commandline.commandRegistry
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -34,13 +36,16 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class ShadeDisplaysRepositoryTest : SysuiTestCase() {
+class ShadePrimaryDisplayCommandTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val commandRegistry = kosmos.commandRegistry
+    private val displayRepository = kosmos.displayRepository
+    private val shadeDisplaysRepository = ShadeDisplaysRepositoryImpl()
     private val pw = PrintWriter(StringWriter())
 
-    private val underTest = ShadeDisplaysRepositoryImpl(commandRegistry)
+    private val underTest =
+        ShadePrimaryDisplayCommand(commandRegistry, displayRepository, shadeDisplaysRepository)
 
     @Before
     fun setUp() {
@@ -50,7 +55,7 @@
     @Test
     fun commandDisplayOverride_updatesDisplayId() =
         testScope.runTest {
-            val displayId by collectLastValue(underTest.displayId)
+            val displayId by collectLastValue(shadeDisplaysRepository.displayId)
             assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
 
             val newDisplayId = 2
@@ -65,7 +70,7 @@
     @Test
     fun commandShadeDisplayOverride_resetsDisplayId() =
         testScope.runTest {
-            val displayId by collectLastValue(underTest.displayId)
+            val displayId by collectLastValue(shadeDisplaysRepository.displayId)
             assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
 
             val newDisplayId = 2
@@ -78,4 +83,17 @@
             commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", "reset"))
             assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
         }
+
+    @Test
+    fun commandShadeDisplayOverride_anyExternalDisplay_notOnDefaultAnymore() =
+        testScope.runTest {
+            val displayId by collectLastValue(shadeDisplaysRepository.displayId)
+            assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+            val newDisplayId = 2
+            displayRepository.addDisplay(displayId = newDisplayId)
+
+            commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", "any_external"))
+
+            assertThat(displayId).isEqualTo(newDisplayId)
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index fed4a26..4f73a345 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -164,10 +164,8 @@
 
     @Provides
     @IntoMap
-    @ClassKey(ShadeDisplaysRepositoryImpl::class)
-    fun provideShadePositionRepositoryAsCoreStartable(
-        impl: ShadeDisplaysRepositoryImpl
-    ): CoreStartable {
+    @ClassKey(ShadePrimaryDisplayCommand::class)
+    fun provideShadePrimaryDisplayCommand(impl: ShadePrimaryDisplayCommand): CoreStartable {
         return if (ShadeWindowGoesAround.isEnabled) {
             impl
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
index 506b4e9..a5d9e96 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
@@ -17,40 +17,107 @@
 package com.android.systemui.shade
 
 import android.view.Display
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.display.data.repository.DisplayRepository
 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.statusbar.commandline.Command
+import com.android.systemui.statusbar.commandline.CommandRegistry
 import java.io.PrintWriter
+import javax.inject.Inject
 
-class ShadePrimaryDisplayCommand(private val positionRepository: ShadeDisplaysRepository) :
-    Command {
+@SysUISingleton
+class ShadePrimaryDisplayCommand
+@Inject
+constructor(
+    private val commandRegistry: CommandRegistry,
+    private val displaysRepository: DisplayRepository,
+    private val positionRepository: ShadeDisplaysRepository,
+) : Command, CoreStartable {
 
-    override fun execute(pw: PrintWriter, args: List<String>) {
-        if (args[0].lowercase() == "reset") {
-            positionRepository.resetDisplayId()
-            pw.println("Reset shade primary display id to ${Display.DEFAULT_DISPLAY}")
-            return
-        }
-
-        val displayId: Int =
-            try {
-                args[0].toInt()
-            } catch (e: NumberFormatException) {
-                pw.println("Error: task id should be an integer")
-                return
-            }
-
-        if (displayId < 0) {
-            pw.println("Error: display id should be positive integer")
-        }
-
-        positionRepository.setDisplayId(displayId)
-        pw.println("New shade primary display id is $displayId")
+    override fun start() {
+        commandRegistry.registerCommand("shade_display_override") { this }
     }
 
     override fun help(pw: PrintWriter) {
         pw.println("shade_display_override <displayId> ")
         pw.println("Set the display which is holding the shade.")
+        pw.println()
         pw.println("shade_display_override reset ")
         pw.println("Reset the display which is holding the shade.")
+        pw.println()
+        pw.println("shade_display_override (list|status) ")
+        pw.println("Lists available displays and which has the shade")
+        pw.println()
+        pw.println("shade_display_override any_external")
+        pw.println("Moves the shade to the first not-default display available")
+    }
+
+    override fun execute(pw: PrintWriter, args: List<String>) {
+        CommandHandler(pw, args).execute()
+    }
+
+    /** Wrapper class to avoid propagating [PrintWriter] to all methods. */
+    private inner class CommandHandler(
+        private val pw: PrintWriter,
+        private val args: List<String>,
+    ) {
+
+        fun execute() {
+            when (val command = args.getOrNull(0)?.lowercase()) {
+                "reset" -> reset()
+                "list",
+                "status" -> printStatus()
+                "any_external" -> anyExternal()
+                else -> {
+                    val cmdAsInteger = command?.toIntOrNull()
+                    if (cmdAsInteger != null) {
+                        changeDisplay(displayId = cmdAsInteger)
+                    } else {
+                        help(pw)
+                    }
+                }
+            }
+        }
+
+        private fun reset() {
+            positionRepository.resetDisplayId()
+            pw.println("Reset shade primary display id to ${Display.DEFAULT_DISPLAY}")
+        }
+
+        private fun printStatus() {
+            val displays = displaysRepository.displays.value
+            val shadeDisplay = positionRepository.displayId.value
+            pw.println("Available displays: ")
+            displays.forEach {
+                pw.print(" - ${it.displayId}")
+                pw.println(if (it.displayId == shadeDisplay) " (Shade window is here)" else "")
+            }
+        }
+
+        private fun anyExternal() {
+            val anyExternalDisplay =
+                displaysRepository.displays.value.firstOrNull {
+                    it.displayId != Display.DEFAULT_DISPLAY
+                }
+            if (anyExternalDisplay == null) {
+                pw.println("No external displays available.")
+                return
+            }
+            setDisplay(anyExternalDisplay.displayId)
+        }
+
+        private fun changeDisplay(displayId: Int) {
+            if (displayId < 0) {
+                pw.println("Error: display id should be positive integer")
+            }
+
+            setDisplay(displayId)
+        }
+
+        private fun setDisplay(id: Int) {
+            positionRepository.setDisplayId(id)
+            pw.println("New shade primary display id is $id")
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
index e920aba..4a95e33 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
@@ -17,10 +17,7 @@
 package com.android.systemui.shade.data.repository
 
 import android.view.Display
-import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.shade.ShadePrimaryDisplayCommand
-import com.android.systemui.statusbar.commandline.CommandRegistry
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -41,9 +38,7 @@
 
 /** Source of truth for the display currently holding the shade. */
 @SysUISingleton
-class ShadeDisplaysRepositoryImpl
-@Inject
-constructor(private val commandRegistry: CommandRegistry) : ShadeDisplaysRepository, CoreStartable {
+class ShadeDisplaysRepositoryImpl @Inject constructor() : ShadeDisplaysRepository {
     private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
 
     override val displayId: StateFlow<Int>
@@ -56,10 +51,4 @@
     override fun resetDisplayId() {
         _displayId.value = Display.DEFAULT_DISPLAY
     }
-
-    override fun start() {
-        commandRegistry.registerCommand("shade_display_override") {
-            ShadePrimaryDisplayCommand(this)
-        }
-    }
 }