<lambda>null1 package com.android.onboarding.contracts
2
3 import android.content.Context
4 import android.content.Intent
5 import android.util.Log
6 import androidx.activity.result.ActivityResult
7 import androidx.activity.result.contract.ActivityResultContract
8 import com.android.onboarding.bedsteadonboarding.contractutils.ContractExecutionEligibilityChecker
9 import com.android.onboarding.bedsteadonboarding.contractutils.ContractUtils
10 import com.android.onboarding.nodes.AndroidOnboardingGraphLog
11 import com.android.onboarding.nodes.NodeRef
12 import com.android.onboarding.nodes.OnboardingEvent
13 import java.util.UUID
14
15 /** Onboarding entities that can be launched. */
16 interface Launchable<I> {
17 /** Provides a [Launcher] to use during launches of this entity. */
18 val launcher: Launcher<I>
19 }
20
21 /** Onboarding entities that can be launched for result. */
22 interface LaunchableForResult<I, O> : Launchable<I> {
23 override val launcher: LauncherForResult<I, O>
24 }
25
26 /** A launcher for onboarding entities that can be launched directly. */
27 abstract class Launcher<I> : NodeRef {
28 /**
29 * Optional event hook for intent preparations just after an outgoing intent, [nodeId] and
30 * [outgoingId] are prepared.
31 */
onPrepareIntentnull32 protected open fun onPrepareIntent(nodeId: NodeId, outgoingId: NodeId) {}
33
34 /** Extract a [NodeId] from a given [context]. */
extractNodeIdnull35 protected abstract fun extractNodeId(context: Context): NodeId
36
37 /** Creates an [Intent] for this entity containing the given argument. */
38 protected abstract fun provideIntent(context: Context, input: I): Intent
39
40 /** Create an Intent with the intention of launching the contract without expecting a result. */
41 internal fun createIntentDirectly(context: Context, input: I): Intent {
42 // Injection point when we are passing control out of the current activity
43 // without expecting a result
44 val outgoingId = newOutgoingId()
45 val nodeId = extractNodeId(context)
46 val intent =
47 provideIntent(context, input).apply { putExtra(EXTRA_ONBOARDING_NODE_ID, outgoingId) }
48
49 onPrepareIntent(nodeId, outgoingId)
50
51 AndroidOnboardingGraphLog.log(
52 OnboardingEvent.ActivityNodeExecutedDirectly(
53 sourceNodeId = nodeId,
54 nodeId = outgoingId,
55 nodeComponent = nodeComponent,
56 nodeName = nodeName,
57 argument = input,
58 )
59 )
60
61 ContractExecutionEligibilityChecker.terminateIfNodeIsTriggeredByTestAndIsNotAllowed(
62 context = context,
63 contractIdentifier =
64 ContractUtils.getContractIdentifier(nodeComponent = nodeComponent, nodeName = nodeName),
65 )
66
67 return intent
68 }
69
70 companion object {
71 /**
72 * Create a new ID to be used for the node started by this launchable.
73 *
74 * This is only used when starting a launchable - it is not used when extracting arguments
75 * during the execution of a contract. In that case, the ID is extracted from the activity
76 * intent.
77 */
newOutgoingIdnull78 internal fun newOutgoingId(): Long = UUID.randomUUID().leastSignificantBits
79 }
80 }
81
82 /** A launcher for onboarding entities that can be launched for result. */
83 abstract class LauncherForResult<I, O>(private val tag: String) : Launcher<I>() {
84 protected open var forResultOutGoingId: NodeId = UNKNOWN_NODE_ID
85
86 /**
87 * Fetches the result without starting the activity.
88 *
89 * This can be optionally implemented, and should return null if a result cannot be fetched and
90 * the activity should be started.
91 */
92 protected open fun provideSynchronousResult(
93 context: Context,
94 args: I,
95 ): ActivityResultContract.SynchronousResult<O>? = null
96
97 /** Extracts the result from the low-level representation [ActivityResult]. */
98 protected abstract fun provideResult(result: ActivityResult): O
99
100 /** @see ActivityResultContract.createIntent */
101 internal fun createIntent(context: Context, input: I): Intent {
102 // Injection point when we are passing control out of the current activity
103 val intent = provideIntent(context, input)
104 val nodeId = extractNodeId(context)
105
106 // We should assume forResultOutGoingId is already set because, in
107 // activityResultRegistry.onLaunch, getSynchronousResult is always called first. Then
108 // createIntent may be called afterwards.
109 // We should expect a outgoingId created in getSynchronousResult and store it in
110 // forResultOutGoingId.
111 // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/ComponentActivity.kt
112 if (forResultOutGoingId != UNKNOWN_NODE_ID) {
113 intent.putExtra(EXTRA_ONBOARDING_NODE_ID, forResultOutGoingId)
114 } else {
115 // getSynchronousResult is not called. This may be not called from
116 // activityResultRegistry.onLaunch. We will use the outgoing which was just created in
117 // performCreateIntent.
118 forResultOutGoingId = intent.getLongExtra(EXTRA_ONBOARDING_NODE_ID, UNKNOWN_NODE_ID)
119 Log.w(tag, "getSynchronousResult was not called when creating intent.")
120 }
121
122 AndroidOnboardingGraphLog.log(
123 OnboardingEvent.ActivityNodeExecutedForResult(
124 sourceNodeId = nodeId,
125 nodeId = forResultOutGoingId,
126 nodeComponent = nodeComponent,
127 nodeName = nodeName,
128 argument = input,
129 )
130 )
131 forResultOutGoingId = UNKNOWN_NODE_ID
132 return intent
133 }
134
135 /** @see ActivityResultContract.parseResult */
136 internal fun parseResult(resultCode: Int, intent: Intent?): O {
137 // Injection point when control has returned to the current activity
138
139 val id = intent?.nodeId ?: UNKNOWN_NODE_ID
140
141 val result = provideResult(ActivityResult(resultCode, intent))
142
143 AndroidOnboardingGraphLog.log(
144 OnboardingEvent.ActivityNodeResultReceived(
145 nodeId = id,
146 nodeComponent = nodeComponent,
147 nodeName = nodeName,
148 result = result,
149 )
150 )
151
152 if (result is NodeResult.Failure) {
153 AndroidOnboardingGraphLog.log(OnboardingEvent.ActivityNodeFail(id, result.toString()))
154 }
155
156 return result
157 }
158
159 /** @see ActivityResultContract.getSynchronousResult */
160 internal fun getSynchronousResult(
161 context: Context,
162 input: I,
163 ): ActivityResultContract.SynchronousResult<O>? {
164 // Injection point when making a synchronous call
165
166 val contractIdentifier = ContractUtils.getContractIdentifier(nodeComponent, nodeName)
167 ContractUtils.getContractResultIfNodeIsFakedInTest(context, contractIdentifier)?.let { result ->
168 Log.i(tag, "Contract result fetched for fake node $contractIdentifier is $result")
169 return ActivityResultContract.SynchronousResult(provideResult(result.toActivityResult()))
170 }
171
172 val thisNodeId = extractNodeId(context)
173
174 forResultOutGoingId = newOutgoingId()
175
176 AndroidOnboardingGraphLog.log(
177 OnboardingEvent.ActivityNodeStartExecuteSynchronously(
178 sourceNodeId = thisNodeId,
179 nodeId = forResultOutGoingId,
180 nodeComponent = nodeComponent,
181 nodeName = nodeName,
182 argument = input,
183 )
184 )
185
186 ContractExecutionEligibilityChecker.terminateIfNodeIsTriggeredByTestAndIsNotAllowed(
187 context = context,
188 contractIdentifier =
189 ContractUtils.getContractIdentifier(nodeComponent = nodeComponent, nodeName = nodeName),
190 )
191
192 val result = provideSynchronousResult(context, input)
193 if (result != null) {
194 // Injection point when the synchronous result was used and the activity was skipped
195
196 AndroidOnboardingGraphLog.log(
197 OnboardingEvent.ActivityNodeExecutedSynchronously(
198 nodeId = forResultOutGoingId,
199 nodeComponent = nodeComponent,
200 nodeName = nodeName,
201 result = result.value,
202 )
203 )
204 }
205
206 return result
207 }
208
209 /** Build an [ActivityResultContract] wrapper that delegates to this [LauncherForResult]. */
210 open fun toActivityResultContract(): ActivityResultContract<I, O> =
211 object : ActivityResultContract<I, O>() {
212 override fun createIntent(context: Context, input: I): Intent =
213 this@LauncherForResult.createIntent(context, input)
214
215 override fun parseResult(resultCode: Int, intent: Intent?): O =
216 this@LauncherForResult.parseResult(resultCode, intent)
217
218 override fun getSynchronousResult(context: Context, input: I): SynchronousResult<O>? =
219 this@LauncherForResult.getSynchronousResult(context, input)
220 }
221 }
222