package com.android.onboarding.contracts import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Parcelable import androidx.annotation.RequiresApi import com.android.onboarding.contracts.NodeAwareIntentScope.IntentExtra.Invalid import com.android.onboarding.contracts.NodeAwareIntentScope.IntentExtra.Present import com.android.onboarding.nodes.AndroidOnboardingGraphLog import com.android.onboarding.nodes.OnboardingEvent import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KClass import kotlin.reflect.KProperty /** * @property androidIntent the intent this scope is wrapping for data manipulation * @property strict by default, closing the scope will only fail the node on the graph in case any * invalid extras are detected without throwing an exception, however in strict mode it will throw * as well */ @IntentManipulationDsl class NodeAwareIntentScope( @OnboardingNodeId override val nodeId: NodeId, private val androidIntent: Intent, private val strict: Boolean = false, ) : NodeAware, AutoCloseable { internal sealed interface IntentExtra { data class Present(val value: V) : IntentExtra data class Invalid(val reason: String) : IntentExtra companion object { operator fun invoke(name: String, kClass: KClass, value: T?): IntentExtra = if (value == null) { Invalid("Intent extra [$name: ${kClass.simpleName}] is missing") } else { Present(value) } inline operator fun invoke(name: String, value: T?): IntentExtra = invoke(name, T::class, value) } } abstract class IntentExtraDelegate internal constructor() : ReadOnlyProperty { abstract val value: V final override fun getValue(thisRef: Any?, property: KProperty<*>): V = value companion object { operator fun invoke(provider: () -> V) = object : IntentExtraDelegate() { override val value: V by lazy(provider) } } } inner class OptionalIntentExtraDelegate internal constructor(internal val extra: IntentExtra) : IntentExtraDelegate() { init { if (extra is Invalid<*>) errors.add(extra.reason) } override val value: V? get() = when (extra) { is Present -> extra.value is Invalid -> null } @IntentManipulationDsl val required: RequiredIntentExtraDelegate get() = RequiredIntentExtraDelegate(extra) } inner class RequiredIntentExtraDelegate internal constructor(internal val extra: IntentExtra) : IntentExtraDelegate() { init { if (extra is Invalid<*>) errors.add(extra.reason) } override val value: V get() = when (extra) { is Present -> extra.value is Invalid -> error("Intent extra cannot be resolved: ${extra.reason}") } @IntentManipulationDsl val optional: OptionalIntentExtraDelegate get() = OptionalIntentExtraDelegate(extra) } @IntentManipulationDsl inline fun IntentExtraDelegate.validate( crossinline validator: (T) -> Unit ): IntentExtraDelegate = IntentExtraDelegate { value.also(validator) } @IntentManipulationDsl inline fun IntentExtraDelegate.map( crossinline transform: (T) -> R ): IntentExtraDelegate = IntentExtraDelegate { value.let(transform) } /** Similar to [map], but only calls [transform] on non-null value from the receiver */ @IntentManipulationDsl inline fun IntentExtraDelegate.mapOrNull( crossinline transform: (T) -> R ): IntentExtraDelegate = IntentExtraDelegate { value?.let(transform) } @IntentManipulationDsl inline fun IntentExtraDelegate.zip( other: IntentExtraDelegate, crossinline zip: (T1, T2) -> R, ): IntentExtraDelegate = IntentExtraDelegate { zip(value, other.value) } @IntentManipulationDsl infix fun > IntentExtraDelegate.or( other: E ): IntentExtraDelegate = IntentExtraDelegate { value ?: other.value } @IntentManipulationDsl infix fun IntentExtraDelegate.or(provider: () -> T): IntentExtraDelegate = IntentExtraDelegate { value ?: provider() } @IntentManipulationDsl infix fun IntentExtraDelegate.or(default: T): IntentExtraDelegate = IntentExtraDelegate { value ?: default } @IntentManipulationDsl inline fun (() -> T).map(crossinline transform: (T) -> R): () -> R = { invoke().let(transform) } @IntentManipulationDsl inline fun (() -> T?).mapOrNull(crossinline transform: (T) -> R): () -> R? = { invoke()?.let(transform) } private val errors = mutableSetOf() override fun close() { if (errors.isNotEmpty()) { val reason = errors.joinToString(prefix = "Detected invalid extras:\n\t", separator = "\n\t - ") AndroidOnboardingGraphLog.log(OnboardingEvent.ActivityNodeFail(nodeId, reason)) if (strict) error(reason) } } // region DSL /** * Self-reference for more fluid write access * * ``` * with(IntentScope) { * intent[KEY] = {"value"} * } * ``` */ @IntentManipulationDsl val intent: NodeAwareIntentScope = this /** Provides observable access to [Intent.getAction] */ @IntentManipulationDsl var action: String? get() = androidIntent.action set(value) { value?.let(androidIntent::setAction) } /** Provides observable access to [Intent.getType] */ @IntentManipulationDsl var type: String? get() = androidIntent.type set(value) { value?.let(androidIntent::setType) } /** Provides observable access to [Intent.getData] */ @IntentManipulationDsl var data: Uri? get() = androidIntent.data set(value) { value?.let(androidIntent::setData) } /** Copy over all [extras] to this [NodeAwareIntentScope] */ @IntentManipulationDsl operator fun plusAssign(extras: Bundle) { androidIntent.putExtras(extras) } /** Copy over all extras from [other] to this [NodeAwareIntentScope] */ @IntentManipulationDsl operator fun plusAssign(other: NodeAwareIntentScope) { androidIntent.putExtras(other.androidIntent) } @IntentManipulationDsl operator fun contains(key: String): Boolean = androidIntent.hasExtra(key) // getters @IntentManipulationDsl fun read(serializer: NodeAwareIntentSerializer): IntentExtraDelegate = RequiredIntentExtraDelegate(with(serializer) { read().let(::Present) }) @IntentManipulationDsl fun string(name: String): OptionalIntentExtraDelegate = OptionalIntentExtraDelegate(IntentExtra(name, androidIntent.getStringExtra(name))) @IntentManipulationDsl fun int(name: String): OptionalIntentExtraDelegate = OptionalIntentExtraDelegate( IntentExtra(name, name.takeIf(::contains)?.let { androidIntent.getIntExtra(it, 0) }) ) @IntentManipulationDsl fun boolean(name: String): OptionalIntentExtraDelegate = OptionalIntentExtraDelegate( IntentExtra(name, name.takeIf(::contains)?.let { androidIntent.getBooleanExtra(it, false) }) ) @IntentManipulationDsl fun bundle(name: String): OptionalIntentExtraDelegate = OptionalIntentExtraDelegate(IntentExtra(name, androidIntent.getBundleExtra(name))) @PublishedApi @RequiresApi(Build.VERSION_CODES.TIRAMISU) @IntentManipulationDsl internal fun parcelable( name: String, kClass: KClass, ): OptionalIntentExtraDelegate = OptionalIntentExtraDelegate( IntentExtra(name, kClass, androidIntent.getParcelableExtra(name, kClass.java)) ) @RequiresApi(Build.VERSION_CODES.TIRAMISU) @IntentManipulationDsl inline fun parcelable(name: String): OptionalIntentExtraDelegate = parcelable(name, T::class) @PublishedApi @RequiresApi(Build.VERSION_CODES.TIRAMISU) @IntentManipulationDsl internal fun parcelableArray( name: String, kClass: KClass, kClassArray: KClass>, ): OptionalIntentExtraDelegate> = OptionalIntentExtraDelegate( IntentExtra(name, kClassArray, androidIntent.getParcelableArrayExtra(name, kClass.java)) ) @RequiresApi(Build.VERSION_CODES.TIRAMISU) @IntentManipulationDsl inline fun parcelableArray( name: String ): OptionalIntentExtraDelegate> = parcelableArray(name, T::class, Array::class) // setters /** Extracts a given value logging error on failure */ @PublishedApi @IntentManipulationDsl internal fun (() -> T).extract(key: String, kClass: KClass): Result = runCatching(::invoke).onFailure { errors.add("Argument value for intent extra [$key: ${kClass.simpleName}] is missing") } private inline fun (() -> T).extract(key: String): Result = extract(key, T::class) /** Extracts a given nullable value ensuring successful [Result] always contains non-null value */ @PublishedApi @IntentManipulationDsl internal fun (() -> T?).extractOptional(): Result = runCatching(::invoke).mapCatching(::requireNotNull) @JvmName("setSerializer") @IntentManipulationDsl inline operator fun set( serializer: NodeAwareIntentSerializer, noinline value: () -> T, ) { value.extract(serializer::class.simpleName ?: "NESTED", T::class).onSuccess { with(serializer) { write(it) } } } @JvmName("setSerializerOrNull") @IntentManipulationDsl inline operator fun set( serializer: NodeAwareIntentSerializer, noinline value: () -> T?, ) { value.extractOptional().onSuccess { with(serializer) { write(it) } } } @JvmName("setString") @IntentManipulationDsl operator fun set(key: String, value: () -> String) { value.extract(key).onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setStringOrNull") @IntentManipulationDsl operator fun set(key: String, value: () -> String?) { value.extractOptional().onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setInt") @IntentManipulationDsl operator fun set(key: String, value: () -> Int) { value.extract(key).onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setIntOrNull") @IntentManipulationDsl operator fun set(key: String, value: () -> Int?) { value.extractOptional().onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setBoolean") @IntentManipulationDsl operator fun set(key: String, value: () -> Boolean) { value.extract(key).onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setBooleanOrNull") @IntentManipulationDsl operator fun set(key: String, value: () -> Boolean?) { value.extractOptional().onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setBundle") @IntentManipulationDsl operator fun set(key: String, value: () -> Bundle) { value.extract(key).onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setBundleOrNull") @IntentManipulationDsl operator fun set(key: String, value: () -> Bundle?) { value.extractOptional().onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setParcelable") @IntentManipulationDsl operator fun set(key: String, value: () -> Parcelable) { value.extract(key).onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setParcelableOrNull") @IntentManipulationDsl operator fun set(key: String, value: () -> Parcelable?) { value.extractOptional().onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setParcelableArray") @IntentManipulationDsl operator fun set(key: String, value: () -> Array) { value.extract(key).onSuccess { androidIntent.putExtra(key, it) } } @JvmName("setParcelableArrayOrNull") @IntentManipulationDsl operator fun set(key: String, value: () -> Array?) { value.extractOptional().onSuccess { androidIntent.putExtra(key, it) } } // endregion }