blob: 0c8ff52cd0fcc6d959632e96fd7bcb12701d0f7c [file]
package com.android.onboarding.nodes
import com.android.onboarding.OnboardingProtos
import com.android.onboarding.contracts.annotations.MAX_NODE_NAME_LENGTH
import com.android.onboarding.contracts.annotations.OnboardingNode
import com.google.common.io.BaseEncoding
import java.lang.IllegalArgumentException
import java.lang.IllegalStateException
import java.time.Instant
/** Extra key used when storing a node id */
const val EXTRA_ONBOARDING_NODE_ID = "com.android.onboarding.ONBOARDING_NODE_ID"
/**
* A target for onboarding logs which can register observers to store those logs.
*
* Note that this will only include part of the overall graph, and must be linked with the graph
* from other onboarding components to see a complete view.
*/
interface OnboardingGraphLog {
/** Add an observer for future graph changes. */
fun addObserver(observer: Observer)
/** Remove the observer so it stops receiving updates. */
fun removeObserver(observer: Observer)
/**
* Log an Onboarding event.
*
* This will be propagated to all observers.
*/
fun log(event: OnboardingEvent)
/** Interface to implement to observe [OnboardingEvent] instances. */
interface Observer {
/** Called when an [OnboardingEvent] occurs. */
fun onEvent(event: OnboardingEvent)
}
// LINT.IfChange
/** Possible Events. */
sealed class OnboardingEvent(open val nodeId: Long, open val nodeName: String?) {
/** Writes this event to a string which can be parsed by [OnboardingGraphLog#deserialize]. */
fun serializeToString(): String {
return BaseEncoding.base64().encode(serialize().toByteArray())
}
abstract fun serialize(): OnboardingProtos.LogProto
// Activities
/**
* At [timestamp], an [android.app.Activity] with id [nodeId] has been launched by invoking the
* [nodeName] contract from inside the [sourceNodeId] node with [argument] without expecting a
* result.
*/
data class ActivityNodeExecutedDirectly
private constructor(
val sourceNodeId: Long,
override val nodeId: Long,
override val nodeName: String,
val argument: Any? = null,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
sourceNodeId: Long,
nodeId: Long,
nodeClass: Class<*>,
argument: Any? = null,
timestamp: Instant = Instant.now()
) : this(sourceNodeId, nodeId, extractNodeNameFromClass(nodeClass), argument, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeExecutedDirectly(
OnboardingProtos.ActivityNodeExecutedDirectlyProto.newBuilder()
.setSourceNodeId(sourceNodeId)
.setNodeId(nodeId)
.setNodeName(nodeName)
// .setArgument(argument)
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeExecutedDirectlyProto) =
ActivityNodeExecutedDirectly(
sourceNodeId = proto.sourceNodeId,
nodeId = proto.nodeId,
nodeName = proto.nodeName,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* At [timestamp], an [android.app.Activity] with id [nodeId] has been launched by invoking the
* [nodeName] contract from inside the [sourceNodeId] node with [argument] while expecting a
* result.
*/
data class ActivityNodeExecutedForResult
private constructor(
val sourceNodeId: Long,
override val nodeId: Long,
override val nodeName: String,
val argument: Any? = null,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
sourceNodeId: Long,
nodeId: Long,
nodeClass: Class<*>,
argument: Any? = null,
timestamp: Instant = Instant.now()
) : this(sourceNodeId, nodeId, extractNodeNameFromClass(nodeClass), argument, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeExecutedForResult(
OnboardingProtos.ActivityNodeExecutedForResultProto.newBuilder()
.setSourceNodeId(sourceNodeId)
.setNodeId(nodeId)
.setNodeName(nodeName)
// .setArgument(argument)
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeExecutedForResultProto) =
ActivityNodeExecutedForResult(
sourceNodeId = proto.sourceNodeId,
nodeId = proto.nodeId,
nodeName = proto.nodeName,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* At [timestamp], an [android.app.Activity] with id [nodeId] is using the [nodeName] contract
* to validate intent [intent].
*/
data class ActivityNodeValidating
private constructor(
override val nodeId: Long,
override val nodeName: String,
val intent: IntentData? = null,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
nodeId: Long,
nodeClass: Class<*>,
intent: IntentData? = null,
timestamp: Instant = Instant.now()
) : this(nodeId, extractNodeNameFromClass(nodeClass), intent, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeValidating(
OnboardingProtos.ActivityNodeValidatingProto.newBuilder()
.setNodeId(nodeId)
.setNodeName(nodeName)
// .setIntent(intent)
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeValidatingProto) =
ActivityNodeValidating(
nodeId = proto.nodeId,
nodeName = proto.nodeName,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* At [timestamp], an [android.app.Activity] with id [nodeId] has failed validation of [intent]
* using the [nodeName] contract. The exception was [exception].
*/
data class ActivityNodeFailedValidation
private constructor(
override val nodeId: Long,
override val nodeName: String,
val exception: Exception = IllegalArgumentException("Failed validation"),
val intent: IntentData? = null,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
nodeId: Long,
nodeClass: Class<*>,
exception: Exception = IllegalArgumentException("Failed validation"),
intent: IntentData? = null,
timestamp: Instant = Instant.now()
) : this(nodeId, extractNodeNameFromClass(nodeClass), exception, intent, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeFailedValidation(
OnboardingProtos.ActivityNodeFailedValidationProto.newBuilder()
.setNodeId(nodeId)
.setNodeName(nodeName)
// .setException(exception)
// .setIntent(IntentData(intent?.action ?: "", mapOf()))
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeFailedValidationProto) =
ActivityNodeFailedValidation(
nodeId = proto.nodeId,
nodeName = proto.nodeName,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* At [timestamp], an [android.app.Activity] with id [nodeId] is extracting the argument from
* [intent] using the [nodeName] contract.
*/
data class ActivityNodeExtractArgument
private constructor(
override val nodeId: Long,
override val nodeName: String,
val intent: IntentData,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
nodeId: Long,
nodeClass: Class<*>,
intent: IntentData,
timestamp: Instant = Instant.now()
) : this(nodeId, extractNodeNameFromClass(nodeClass), intent, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeExtractArgument(
OnboardingProtos.ActivityNodeExtractArgumentProto.newBuilder()
.setNodeId(nodeId)
.setNodeName(nodeName)
.setIntent(
OnboardingProtos.IntentDataProto.newBuilder().setAction(intent.action ?: "").build()
)
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeExtractArgumentProto) =
ActivityNodeExtractArgument(
nodeId = proto.nodeId,
nodeName = proto.nodeName,
intent =
IntentData(
if (proto.hasIntent()) proto.intent.action else "",
mapOf()
),
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* At [timestamp], an [android.app.Activity] with id [nodeId] has extracted [argument] using the
* [nodeName] contract.
*/
data class ActivityNodeArgumentExtracted
private constructor(
override val nodeId: Long,
override val nodeName: String,
val argument: Any?,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
nodeId: Long,
nodeClass: Class<*>,
argument: Any? = null,
timestamp: Instant = Instant.now()
) : this(nodeId, extractNodeNameFromClass(nodeClass), argument, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeArgumentExtracted(
OnboardingProtos.ActivityNodeArgumentExtractedProto.newBuilder()
.setNodeId(nodeId)
.setNodeName(nodeName)
// setArgument(argument)
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeArgumentExtractedProto) =
ActivityNodeArgumentExtracted(
nodeId = proto.nodeId,
nodeName = proto.nodeName,
argument = null,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* At [timestamp], an [android.app.Activity] with id [nodeId] has set the result to [result]
* using the [nodeName] contract.
*/
data class ActivityNodeSetResult
private constructor(
override val nodeId: Long,
override val nodeName: String,
val result: Any?,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
nodeId: Long,
nodeClass: Class<*>,
result: Any?,
timestamp: Instant = Instant.now()
) : this(nodeId, extractNodeNameFromClass(nodeClass), result, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeSetResult(
OnboardingProtos.ActivityNodeSetResultProto.newBuilder()
.setNodeId(nodeId)
.setNodeName(nodeName)
// setResult(result)
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeSetResultProto) =
ActivityNodeSetResult(
nodeId = proto.nodeId,
nodeName = proto.nodeName,
result = null,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* At [timestamp], an [android.app.Activity] with id [nodeId] has failed the [nodeName] contract
* because of [reason].
*/
data class ActivityNodeFail(
override val nodeId: Long,
val reason: String?,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName = null) {
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeFail(
OnboardingProtos.ActivityNodeFailProto.newBuilder()
.setNodeId(nodeId)
.setReason(reason ?: "")
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeFailProto) =
ActivityNodeFail(
nodeId = proto.nodeId,
reason = proto.reason,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* At [timestamp], the [result] of the [android.app.Activity] node with id [nodeId] was
* received.
*
* Note that this does not specify which node received the result. The event must be matched up
* with a previous [ActivityNodeExecutedForResult] event.
*/
data class ActivityNodeResultReceived
private constructor(
override val nodeId: Long,
override val nodeName: String,
val result: Any?,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
nodeId: Long,
nodeClass: Class<*>,
result: Any?,
timestamp: Instant = Instant.now()
) : this(nodeId, extractNodeNameFromClass(nodeClass), result, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeResultReceived(
OnboardingProtos.ActivityNodeResultReceivedProto.newBuilder()
.setNodeId(nodeId)
.setNodeName(nodeName)
// setResult(result)
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeResultReceivedProto) =
ActivityNodeResultReceived(
nodeId = proto.nodeId,
nodeName = proto.nodeName,
result = null,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* At [timestamp], the node with id [sourceNodeId] began synchronous execution of the node with
* id [nodeId].
*/
data class ActivityNodeStartExecuteSynchronously
private constructor(
val sourceNodeId: Long,
override val nodeId: Long,
override val nodeName: String,
val argument: Any?,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
sourceNodeId: Long,
nodeId: Long,
nodeClass: Class<*>,
argument: Any? = null,
timestamp: Instant = Instant.now()
) : this(sourceNodeId, nodeId, extractNodeNameFromClass(nodeClass), argument, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeStartExecuteSynchronously(
OnboardingProtos.ActivityNodeStartExecuteSynchronouslyProto.newBuilder()
.setSourceNodeId(sourceNodeId)
.setNodeId(nodeId)
.setNodeName(nodeName)
// setArgument(argument)
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeStartExecuteSynchronouslyProto) =
ActivityNodeStartExecuteSynchronously(
sourceNodeId = proto.sourceNodeId,
nodeId = proto.nodeId,
nodeName = proto.nodeName,
argument = null,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/** At [timestamp], the synchronously executing node with id [nodeId] got result [result]. */
data class ActivityNodeExecutedSynchronously
private constructor(
override val nodeId: Long,
override val nodeName: String,
val result: Any?,
val timestamp: Instant = Instant.now()
) : OnboardingEvent(nodeId, nodeName) {
constructor(
nodeId: Long,
nodeClass: Class<*>,
result: Any?,
timestamp: Instant = Instant.now()
) : this(nodeId, extractNodeNameFromClass(nodeClass), result, timestamp)
override fun serialize(): OnboardingProtos.LogProto {
return OnboardingProtos.LogProto.newBuilder()
.setActivityNodeExecutedSynchronously(
OnboardingProtos.ActivityNodeExecutedSynchronouslyProto.newBuilder()
.setNodeId(nodeId)
.setNodeName(nodeName)
// setResult(result)
.setTimestamp(timestamp.toEpochMilli())
.build()
)
.build()
}
companion object {
fun fromProto(proto: OnboardingProtos.ActivityNodeExecutedSynchronouslyProto) =
ActivityNodeExecutedSynchronously(
nodeId = proto.nodeId,
nodeName = proto.nodeName,
result = null,
timestamp = Instant.ofEpochMilli(proto.timestamp)
)
}
}
/**
* Wrapper around the same information as [android.content.Intent] but can be used without
* dependency on Android.
*/
data class IntentData(val action: String?, val extras: Map<String, Any?>)
companion object {
private fun extractNodeNameFromClass(nodeClass: Class<*>): String =
nodeClass.getAnnotation(OnboardingNode::class.java)?.name?.also {
require(it.length <= MAX_NODE_NAME_LENGTH) {
"Node name length (${it.length}) exceeds maximum length of $MAX_NODE_NAME_LENGTH characters"
}
} ?: throw IllegalArgumentException("All nodes must be annotated @OnboardingNode")
fun deserialize(str: String): OnboardingEvent {
val proto = OnboardingProtos.LogProto.parseFrom(BaseEncoding.base64().decode(str))
if (proto.hasActivityNodeExecutedDirectly()) {
return ActivityNodeExecutedDirectly.fromProto(proto.activityNodeExecutedDirectly)
} else if (proto.hasActivityNodeExecutedForResult()) {
return ActivityNodeExecutedForResult.fromProto(proto.activityNodeExecutedForResult)
} else if (proto.hasActivityNodeValidating()) {
return ActivityNodeValidating.fromProto(proto.activityNodeValidating)
} else if (proto.hasActivityNodeFailedValidation()) {
return ActivityNodeFailedValidation.fromProto(proto.activityNodeFailedValidation)
} else if (proto.hasActivityNodeExtractArgument()) {
return ActivityNodeExtractArgument.fromProto(proto.activityNodeExtractArgument)
} else if (proto.hasActivityNodeArgumentExtracted()) {
return ActivityNodeArgumentExtracted.fromProto(proto.activityNodeArgumentExtracted)
} else if (proto.hasActivityNodeSetResult()) {
return ActivityNodeSetResult.fromProto(proto.activityNodeSetResult)
} else if (proto.hasActivityNodeFail()) {
return ActivityNodeFail.fromProto(proto.activityNodeFail)
} else if (proto.hasActivityNodeResultReceived()) {
return ActivityNodeResultReceived.fromProto(proto.activityNodeResultReceived)
} else if (proto.hasActivityNodeStartExecuteSynchronously()) {
return ActivityNodeStartExecuteSynchronously.fromProto(
proto.activityNodeStartExecuteSynchronously
)
} else if (proto.hasActivityNodeExecutedSynchronously()) {
return ActivityNodeExecutedSynchronously.fromProto(
proto.activityNodeExecutedSynchronously
)
}
throw IllegalStateException("Could not parse $proto")
}
}
}
// LINT.ThenChange(//depot/google3/logs/proto/android_onboarding/node_interaction_metadata.proto)
}