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