Move Kosmos to its own build module

Bug: 302691505
Flag: NA
Test: atest
Change-Id: I0f2fbf525fb37133f801d07e62eb3f4cf89e26c6
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index dc7850f..8113be1 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -198,12 +198,29 @@
 }
 
 filegroup {
+    name: "kosmos-src",
+    srcs: ["tests/utils/kosmos/src/**/*.kt"],
+    path: "tests/utils/kosmos",
+}
+
+java_library {
+    name: "kosmos",
+    host_supported: true,
+    srcs: [":kosmos-src"],
+    static_libs: [
+        "kotlin-reflect",
+        "kotlin-stdlib",
+    ],
+}
+
+filegroup {
     name: "SystemUI-tests-utils",
     srcs: [
         "tests/utils/src/**/*.java",
         "tests/utils/src/**/*.kt",
+        ":kosmos-src",
     ],
-    path: "tests/utils/src",
+    path: "tests/utils",
 }
 
 filegroup {
@@ -336,6 +353,7 @@
         "kotlin-test",
         "SystemUICustomizationTestUtils",
         "androidx.compose.runtime_runtime",
+        "kosmos",
     ],
     libs: [
         "android.test.runner",
@@ -419,6 +437,7 @@
         "inline-mockito-robolectric-prebuilt",
         "platform-parametric-runner-lib",
         "SystemUICustomizationTestUtils",
+        "kosmos",
     ],
     libs: [
         "android.test.runner",
@@ -452,6 +471,7 @@
         "androidx.test.uiautomator_uiautomator",
         "androidx.core_core-animation-testing",
         "androidx.test.ext.junit",
+        "kosmos",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/tests/utils/kosmos/src/com/android/systemui/kosmos/Kosmos.kt b/packages/SystemUI/tests/utils/kosmos/src/com/android/systemui/kosmos/Kosmos.kt
new file mode 100644
index 0000000..685bb0a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/kosmos/src/com/android/systemui/kosmos/Kosmos.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.kosmos
+
+import kotlin.reflect.KProperty
+
+// (Historical note: The name Kosmos is meant to invoke "Kotlin", the "Object Mother" pattern
+//   (https://martinfowler.com/bliki/ObjectMother.html), and of course the Greek word "kosmos" for
+//   the "order of the world" (https://en.wiktionary.org/wiki/%CE%BA%CF%8C%CF%83%CE%BC%CE%BF%CF%82)
+
+/**
+ * Each Kosmos is its own self-contained set of fixtures, which may reference each other. Fixtures
+ * can be defined through extension properties in any file:
+ * ```
+ * // fixture that must be set:
+ * var Kosmos.context by Fixture<Context>()
+ *
+ * // fixture with overrideable default.
+ * var Kosmos.landscapeMode by Fixture { false }
+ *
+ * // fixture forbidding override (note `val`, and referencing context fixture from above)
+ * val Kosmos.lifecycleScope by Fixture { context.lifecycleScope }
+ * ```
+ *
+ * To use the fixtures, create an instance of Kosmos and retrieve the values you need:
+ * ```
+ * val k = Kosmos()
+ * k.context = mContext
+ * val underTest = YourInteractor(
+ *     context = k.context,
+ *     landscapeMode = k.landscapeMode,
+ * )
+ * ```
+ */
+interface Kosmos {
+    /**
+     * Lookup a fixture in the Kosmos by [name], using [creator] to instantiate and store one if
+     * there is none present.
+     */
+    fun <T> get(name: String, creator: (Kosmos.() -> T)?): T
+
+    /** Sets the [value] of a fixture with the given [name]. */
+    fun set(name: String, value: Any?)
+
+    /**
+     * A value in the kosmos that has a single value once it's read. It can be overridden before
+     * first use only; all objects that are dependent on this fixture will get the same value.
+     *
+     * Example classic uses would be a clock, filesystem, or singleton controller.
+     *
+     * If no [creator] parameter is provided, the fixture must be set before use.
+     */
+    class Fixture<T>(private val creator: (Kosmos.() -> T)? = null) {
+        operator fun getValue(thisRef: Kosmos, property: KProperty<*>): T =
+            thisRef.get(property.name, creator)
+
+        operator fun setValue(thisRef: Kosmos, property: KProperty<*>, value: T) {
+            thisRef.set(property.name, value)
+        }
+    }
+}
+
+/** Constructs a fresh Kosmos. */
+fun Kosmos(): Kosmos = KosmosRegistry()
+
+private class KosmosRegistry : Kosmos {
+    val map: MutableMap<String, Any?> = mutableMapOf()
+    val gotten: MutableSet<String> = mutableSetOf()
+
+    override fun <T> get(name: String, creator: (Kosmos.() -> T)?): T {
+        gotten.add(name)
+        if (name !in map) {
+            checkNotNull(creator) { "Fixture $name has no default, and is read before set." }
+            map[name] = creator()
+        }
+        @Suppress("UNCHECKED_CAST") return map[name] as T
+    }
+
+    override fun set(name: String, value: Any?) {
+        check(name !in gotten) { "Tried to set fixture '$name' after it's already been read." }
+        map[name] = value
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/Kosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/Kosmos.kt
deleted file mode 100644
index c74cdd4..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/Kosmos.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.android.systemui.kosmos
-
-import kotlin.reflect.KProperty
-
-// (Historical note: The name Kosmos is meant to invoke "Kotlin", the "Object Mother" pattern
-//   (https://martinfowler.com/bliki/ObjectMother.html), and of course the Greek word "kosmos" for
-//   the "order of the world" (https://en.wiktionary.org/wiki/%CE%BA%CF%8C%CF%83%CE%BC%CE%BF%CF%82)
-/**
- * Each Kosmos is its own self-contained set of fixtures, which may reference each other. Fixtures
- * can be defined through extension properties in any file:
- * ```
- * // fixture that must be set:
- * var Kosmos.context by Fixture<Context>()
- *
- * // fixture with overrideable default.
- * var Kosmos.landscapeMode by Fixture { false }
- *
- * // fixture forbidding override (note `val`, and referencing context fixture from above)
- * val Kosmos.lifecycleScope by Fixture { context.lifecycleScope }
- * ```
- *
- * To use the fixtures, create an instance of Kosmos and retrieve the values you need:
- * ```
- * val k = Kosmos()
- * k.context = mContext
- * val underTest = YourInteractor(
- *     context = k.context,
- *     landscapeMode = k.landscapeMode,
- * )
- * ```
- */
-class Kosmos {
-    private val map: MutableMap<String, Any?> = mutableMapOf()
-    private val gotten: MutableSet<String> = mutableSetOf()
-
-    /**
-     * A value in the kosmos that has a single value once it's read. It can be overridden before
-     * first use only; all objects that are dependent on this fixture will get the same value.
-     *
-     * Example classic uses would be a clock, filesystem, or singleton controller.
-     *
-     * If no [creator] parameter is provided, the fixture must be set before use.
-     */
-    class Fixture<T>(private val creator: (Kosmos.() -> T)? = null) {
-        operator fun getValue(thisRef: Kosmos, property: KProperty<*>): T {
-            thisRef.gotten.add(property.name)
-            @Suppress("UNCHECKED_CAST")
-            if (!thisRef.map.contains(property.name)) {
-                if (creator == null) {
-                    throw IllegalStateException(
-                        "Fixture ${property.name} has no default, and is read before set."
-                    )
-                } else {
-                    val nonNullCreator = creator
-                    // The Kotlin compiler seems to need this odd workaround
-                    thisRef.map[property.name] = thisRef.nonNullCreator()
-                }
-            }
-            return thisRef.map[property.name] as T
-        }
-
-        operator fun setValue(thisRef: Kosmos, property: KProperty<*>, value: T) {
-            check(!thisRef.gotten.contains(property.name)) {
-                "Tried to set fixture '${property.name}' after it's already been read."
-            }
-            thisRef.map[property.name] = value
-        }
-    }
-}