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