xref: /aosp_15_r20/external/android_onboarding/java/com/android/onboarding/contracts/NodeAwareIntentScope.kt (revision c625018464ae97c56936c82b1b617e11aa899faa)
1 package com.android.onboarding.contracts
2 
3 import android.content.Intent
4 import android.net.Uri
5 import android.os.Build
6 import android.os.Bundle
7 import android.os.Parcelable
8 import androidx.annotation.RequiresApi
9 import com.android.onboarding.contracts.NodeAwareIntentScope.IntentExtra.Invalid
10 import com.android.onboarding.contracts.NodeAwareIntentScope.IntentExtra.Present
11 import com.android.onboarding.nodes.AndroidOnboardingGraphLog
12 import com.android.onboarding.nodes.OnboardingEvent
13 import kotlin.properties.ReadOnlyProperty
14 import kotlin.reflect.KClass
15 import kotlin.reflect.KProperty
16 
17 /**
18  * @property androidIntent the intent this scope is wrapping for data manipulation
19  * @property strict by default, closing the scope will only fail the node on the graph in case any
20  *   invalid extras are detected without throwing an exception, however in strict mode it will throw
21  *   as well
22  */
23 @IntentManipulationDsl
24 class NodeAwareIntentScope(
25   @OnboardingNodeId override val nodeId: NodeId,
26   private val androidIntent: Intent,
27   private val strict: Boolean = false,
28 ) : NodeAware, AutoCloseable {
29   internal sealed interface IntentExtra<V : Any> {
30     data class Present<V : Any>(val value: V) : IntentExtra<V>
31 
32     data class Invalid<V : Any>(val reason: String) : IntentExtra<V>
33 
34     companion object {
invokenull35       operator fun <T : Any> invoke(name: String, kClass: KClass<T>, value: T?): IntentExtra<T> =
36         if (value == null) {
37           Invalid("Intent extra [$name: ${kClass.simpleName}] is missing")
38         } else {
39           Present(value)
40         }
41 
invokenull42       inline operator fun <reified T : Any> invoke(name: String, value: T?): IntentExtra<T> =
43         invoke(name, T::class, value)
44     }
45   }
46 
47   abstract class IntentExtraDelegate<V> internal constructor() : ReadOnlyProperty<Any?, V> {
48     abstract val value: V
49 
50     final override fun getValue(thisRef: Any?, property: KProperty<*>): V = value
51 
52     companion object {
53       operator fun <V> invoke(provider: () -> V) =
54         object : IntentExtraDelegate<V>() {
55           override val value: V by lazy(provider)
56         }
57     }
58   }
59 
60   inner class OptionalIntentExtraDelegate<V : Any>
61   internal constructor(internal val extra: IntentExtra<out V>) : IntentExtraDelegate<V?>() {
62     init {
63       if (extra is Invalid<*>) errors.add(extra.reason)
64     }
65 
66     override val value: V?
67       get() =
68         when (extra) {
69           is Present -> extra.value
70           is Invalid -> null
71         }
72 
73     @IntentManipulationDsl
74     val required: RequiredIntentExtraDelegate<V>
75       get() = RequiredIntentExtraDelegate(extra)
76   }
77 
78   inner class RequiredIntentExtraDelegate<V : Any>
79   internal constructor(internal val extra: IntentExtra<out V>) : IntentExtraDelegate<V>() {
80     init {
81       if (extra is Invalid<*>) errors.add(extra.reason)
82     }
83 
84     override val value: V
85       get() =
86         when (extra) {
87           is Present -> extra.value
88           is Invalid -> error("Intent extra cannot be resolved: ${extra.reason}")
89         }
90 
91     @IntentManipulationDsl
92     val optional: OptionalIntentExtraDelegate<V>
93       get() = OptionalIntentExtraDelegate(extra)
94   }
95 
96   @IntentManipulationDsl
validatenull97   inline fun <T> IntentExtraDelegate<T>.validate(
98     crossinline validator: (T) -> Unit
99   ): IntentExtraDelegate<T> = IntentExtraDelegate { value.also(validator) }
100 
101   @IntentManipulationDsl
mapnull102   inline fun <T, R> IntentExtraDelegate<T>.map(
103     crossinline transform: (T) -> R
104   ): IntentExtraDelegate<R> = IntentExtraDelegate { value.let(transform) }
105 
106   /** Similar to [map], but only calls [transform] on non-null value from the receiver */
107   @IntentManipulationDsl
mapOrNullnull108   inline fun <T, R> IntentExtraDelegate<T?>.mapOrNull(
109     crossinline transform: (T) -> R
110   ): IntentExtraDelegate<R?> = IntentExtraDelegate { value?.let(transform) }
111 
112   @IntentManipulationDsl
zipnull113   inline fun <T1, T2, R> IntentExtraDelegate<T1>.zip(
114     other: IntentExtraDelegate<T2>,
115     crossinline zip: (T1, T2) -> R,
116   ): IntentExtraDelegate<R> = IntentExtraDelegate { zip(value, other.value) }
117 
118   @IntentManipulationDsl
ornull119   infix fun <T, E : IntentExtraDelegate<T>> IntentExtraDelegate<T?>.or(
120     other: E
121   ): IntentExtraDelegate<T> = IntentExtraDelegate { value ?: other.value }
122 
123   @IntentManipulationDsl
ornull124   infix fun <T> IntentExtraDelegate<T?>.or(provider: () -> T): IntentExtraDelegate<T> =
125     IntentExtraDelegate {
126       value ?: provider()
127     }
128 
129   @IntentManipulationDsl
ornull130   infix fun <T> IntentExtraDelegate<T?>.or(default: T): IntentExtraDelegate<T> =
131     IntentExtraDelegate {
132       value ?: default
133     }
134 
135   @IntentManipulationDsl
<lambda>null136   inline fun <T, R> (() -> T).map(crossinline transform: (T) -> R): () -> R = {
137     invoke().let(transform)
138   }
139 
140   @IntentManipulationDsl
<lambda>null141   inline fun <T, R> (() -> T?).mapOrNull(crossinline transform: (T) -> R): () -> R? = {
142     invoke()?.let(transform)
143   }
144 
145   private val errors = mutableSetOf<String>()
146 
closenull147   override fun close() {
148     if (errors.isNotEmpty()) {
149       val reason =
150         errors.joinToString(prefix = "Detected invalid extras:\n\t", separator = "\n\t - ")
151       AndroidOnboardingGraphLog.log(OnboardingEvent.ActivityNodeFail(nodeId, reason))
152       if (strict) error(reason)
153     }
154   }
155 
156   // region DSL
157   /**
158    * Self-reference for more fluid write access
159    *
160    * ```
161    * with(IntentScope) {
162    *   intent[KEY] = {"value"}
163    * }
164    * ```
165    */
166   @IntentManipulationDsl val intent: NodeAwareIntentScope = this
167 
168   /** Provides observable access to [Intent.getAction] */
169   @IntentManipulationDsl
170   var action: String?
171     get() = androidIntent.action
172     set(value) {
173       value?.let(androidIntent::setAction)
174     }
175 
176   /** Provides observable access to [Intent.getType] */
177   @IntentManipulationDsl
178   var type: String?
179     get() = androidIntent.type
180     set(value) {
181       value?.let(androidIntent::setType)
182     }
183 
184   /** Provides observable access to [Intent.getData] */
185   @IntentManipulationDsl
186   var data: Uri?
187     get() = androidIntent.data
188     set(value) {
189       value?.let(androidIntent::setData)
190     }
191 
192   /** Copy over all [extras] to this [NodeAwareIntentScope] */
193   @IntentManipulationDsl
plusAssignnull194   operator fun plusAssign(extras: Bundle) {
195     androidIntent.putExtras(extras)
196   }
197 
198   /** Copy over all extras from [other] to this [NodeAwareIntentScope] */
199   @IntentManipulationDsl
plusAssignnull200   operator fun plusAssign(other: NodeAwareIntentScope) {
201     androidIntent.putExtras(other.androidIntent)
202   }
203 
containsnull204   @IntentManipulationDsl operator fun contains(key: String): Boolean = androidIntent.hasExtra(key)
205 
206   // getters
207 
208   @IntentManipulationDsl
209   fun <T : Any> read(serializer: NodeAwareIntentSerializer<T>): IntentExtraDelegate<T> =
210     RequiredIntentExtraDelegate(with(serializer) { read().let(::Present) })
211 
212   @IntentManipulationDsl
stringnull213   fun string(name: String): OptionalIntentExtraDelegate<String> =
214     OptionalIntentExtraDelegate(IntentExtra(name, androidIntent.getStringExtra(name)))
215 
216   @IntentManipulationDsl
217   fun int(name: String): OptionalIntentExtraDelegate<Int> =
218     OptionalIntentExtraDelegate(
219       IntentExtra(name, name.takeIf(::contains)?.let { androidIntent.getIntExtra(it, 0) })
220     )
221 
222   @IntentManipulationDsl
booleannull223   fun boolean(name: String): OptionalIntentExtraDelegate<Boolean> =
224     OptionalIntentExtraDelegate(
225       IntentExtra(name, name.takeIf(::contains)?.let { androidIntent.getBooleanExtra(it, false) })
226     )
227 
228   @IntentManipulationDsl
bundlenull229   fun bundle(name: String): OptionalIntentExtraDelegate<Bundle> =
230     OptionalIntentExtraDelegate(IntentExtra(name, androidIntent.getBundleExtra(name)))
231 
232   @PublishedApi
233   @RequiresApi(Build.VERSION_CODES.TIRAMISU)
234   @IntentManipulationDsl
235   internal fun <T : Any> parcelable(
236     name: String,
237     kClass: KClass<T>,
238   ): OptionalIntentExtraDelegate<T> =
239     OptionalIntentExtraDelegate(
240       IntentExtra(name, kClass, androidIntent.getParcelableExtra(name, kClass.java))
241     )
242 
243   @RequiresApi(Build.VERSION_CODES.TIRAMISU)
244   @IntentManipulationDsl
245   inline fun <reified T : Any> parcelable(name: String): OptionalIntentExtraDelegate<T> =
246     parcelable(name, T::class)
247 
248   @PublishedApi
249   @RequiresApi(Build.VERSION_CODES.TIRAMISU)
250   @IntentManipulationDsl
251   internal fun <T : Any> parcelableArray(
252     name: String,
253     kClass: KClass<T>,
254     kClassArray: KClass<Array<T>>,
255   ): OptionalIntentExtraDelegate<Array<T>> =
256     OptionalIntentExtraDelegate(
257       IntentExtra(name, kClassArray, androidIntent.getParcelableArrayExtra(name, kClass.java))
258     )
259 
260   @RequiresApi(Build.VERSION_CODES.TIRAMISU)
261   @IntentManipulationDsl
262   inline fun <reified T : Any> parcelableArray(
263     name: String
264   ): OptionalIntentExtraDelegate<Array<T>> = parcelableArray(name, T::class, Array<T>::class)
265 
266   // setters
267 
268   /** Extracts a given value logging error on failure */
269   @PublishedApi
270   @IntentManipulationDsl
271   internal fun <T : Any> (() -> T).extract(key: String, kClass: KClass<T>): Result<T> =
272     runCatching(::invoke).onFailure {
273       errors.add("Argument value for intent extra [$key: ${kClass.simpleName}] is missing")
274     }
275 
extractnull276   private inline fun <reified T : Any> (() -> T).extract(key: String): Result<T> =
277     extract(key, T::class)
278 
279   /** Extracts a given nullable value ensuring successful [Result] always contains non-null value */
280   @PublishedApi
281   @IntentManipulationDsl
282   internal fun <T : Any> (() -> T?).extractOptional(): Result<T> =
283     runCatching(::invoke).mapCatching(::requireNotNull)
284 
285   @JvmName("setSerializer")
286   @IntentManipulationDsl
287   inline operator fun <reified T : Any> set(
288     serializer: NodeAwareIntentSerializer<T>,
289     noinline value: () -> T,
290   ) {
291     value.extract(serializer::class.simpleName ?: "NESTED", T::class).onSuccess {
292       with(serializer) { write(it) }
293     }
294   }
295 
296   @JvmName("setSerializerOrNull")
297   @IntentManipulationDsl
setnull298   inline operator fun <reified T : Any> set(
299     serializer: NodeAwareIntentSerializer<T>,
300     noinline value: () -> T?,
301   ) {
302     value.extractOptional().onSuccess { with(serializer) { write(it) } }
303   }
304 
305   @JvmName("setString")
306   @IntentManipulationDsl
setnull307   operator fun set(key: String, value: () -> String) {
308     value.extract(key).onSuccess { androidIntent.putExtra(key, it) }
309   }
310 
311   @JvmName("setStringOrNull")
312   @IntentManipulationDsl
setnull313   operator fun set(key: String, value: () -> String?) {
314     value.extractOptional().onSuccess { androidIntent.putExtra(key, it) }
315   }
316 
317   @JvmName("setInt")
318   @IntentManipulationDsl
setnull319   operator fun set(key: String, value: () -> Int) {
320     value.extract(key).onSuccess { androidIntent.putExtra(key, it) }
321   }
322 
323   @JvmName("setIntOrNull")
324   @IntentManipulationDsl
setnull325   operator fun set(key: String, value: () -> Int?) {
326     value.extractOptional().onSuccess { androidIntent.putExtra(key, it) }
327   }
328 
329   @JvmName("setBoolean")
330   @IntentManipulationDsl
setnull331   operator fun set(key: String, value: () -> Boolean) {
332     value.extract(key).onSuccess { androidIntent.putExtra(key, it) }
333   }
334 
335   @JvmName("setBooleanOrNull")
336   @IntentManipulationDsl
setnull337   operator fun set(key: String, value: () -> Boolean?) {
338     value.extractOptional().onSuccess { androidIntent.putExtra(key, it) }
339   }
340 
341   @JvmName("setBundle")
342   @IntentManipulationDsl
setnull343   operator fun set(key: String, value: () -> Bundle) {
344     value.extract(key).onSuccess { androidIntent.putExtra(key, it) }
345   }
346 
347   @JvmName("setBundleOrNull")
348   @IntentManipulationDsl
setnull349   operator fun set(key: String, value: () -> Bundle?) {
350     value.extractOptional().onSuccess { androidIntent.putExtra(key, it) }
351   }
352 
353   @JvmName("setParcelable")
354   @IntentManipulationDsl
setnull355   operator fun set(key: String, value: () -> Parcelable) {
356     value.extract(key).onSuccess { androidIntent.putExtra(key, it) }
357   }
358 
359   @JvmName("setParcelableOrNull")
360   @IntentManipulationDsl
setnull361   operator fun set(key: String, value: () -> Parcelable?) {
362     value.extractOptional().onSuccess { androidIntent.putExtra(key, it) }
363   }
364 
365   @JvmName("setParcelableArray")
366   @IntentManipulationDsl
setnull367   operator fun set(key: String, value: () -> Array<out Parcelable>) {
368     value.extract(key).onSuccess { androidIntent.putExtra(key, it) }
369   }
370 
371   @JvmName("setParcelableArrayOrNull")
372   @IntentManipulationDsl
setnull373   operator fun set(key: String, value: () -> Array<out Parcelable>?) {
374     value.extractOptional().onSuccess { androidIntent.putExtra(key, it) }
375   }
376 
377   // endregion
378 }
379