1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.kosmos
18 
19 import kotlin.reflect.KProperty
20 
21 // (Historical note: The name Kosmos is meant to invoke "Kotlin", the "Object Mother" pattern
22 //   (https://martinfowler.com/bliki/ObjectMother.html), and of course the Greek word "kosmos" for
23 //   the "order of the world" (https://en.wiktionary.org/wiki/%CE%BA%CF%8C%CF%83%CE%BC%CE%BF%CF%82)
24 
25 /**
26  * Each Kosmos is its own self-contained set of fixtures, which may reference each other. Fixtures
27  * can be defined through extension properties in any file:
28  * ```
29  * // fixture that must be set:
30  * var Kosmos.context by Fixture<Context>()
31  *
32  * // fixture with overrideable default.
33  * var Kosmos.landscapeMode by Fixture { false }
34  *
35  * // fixture forbidding override (note `val`, and referencing context fixture from above)
36  * val Kosmos.lifecycleScope by Fixture { context.lifecycleScope }
37  * ```
38  *
39  * To use the fixtures, create an instance of Kosmos and retrieve the values you need:
40  * ```
41  * val k = Kosmos()
42  * k.context = mContext
43  * val underTest = YourInteractor(
44  *     context = k.context,
45  *     landscapeMode = k.landscapeMode,
46  * )
47  * ```
48  */
49 interface Kosmos {
50     /**
51      * Lookup a fixture in the Kosmos by [name], using [creator] to instantiate and store one if
52      * there is none present.
53      */
getnull54     fun <T> get(name: String, creator: (Kosmos.() -> T)?): T
55 
56     /** Sets the [value] of a fixture with the given [name]. */
57     fun set(name: String, value: Any?)
58 
59     /**
60      * A value in the kosmos that has a single value once it's read. It can be overridden before
61      * first use only; all objects that are dependent on this fixture will get the same value.
62      *
63      * Example classic uses would be a clock, filesystem, or singleton controller.
64      *
65      * If no [creator] parameter is provided, the fixture must be set before use.
66      */
67     class Fixture<T>(private val creator: (Kosmos.() -> T)? = null) {
68         operator fun getValue(thisRef: Kosmos, property: KProperty<*>): T =
69             thisRef.get(property.name, creator)
70 
71         operator fun setValue(thisRef: Kosmos, property: KProperty<*>, value: T) {
72             thisRef.set(property.name, value)
73         }
74     }
75 }
76 
77 /** Constructs a fresh Kosmos. */
Kosmosnull78 fun Kosmos(): Kosmos = KosmosRegistry()
79 
80 private class KosmosRegistry : Kosmos {
81     val map: MutableMap<String, Any?> = mutableMapOf()
82     val gotten: MutableSet<String> = mutableSetOf()
83 
84     override fun <T> get(name: String, creator: (Kosmos.() -> T)?): T {
85         gotten.add(name)
86         if (name !in map) {
87             checkNotNull(creator) { "Fixture $name has no default, and is read before set." }
88             map[name] = creator()
89         }
90         @Suppress("UNCHECKED_CAST") return map[name] as T
91     }
92 
93     override fun set(name: String, value: Any?) {
94         check(name !in gotten) { "Tried to set fixture '$name' after it's already been read." }
95         map[name] = value
96     }
97 }
98