xref: /aosp_15_r20/external/android_onboarding/java/com/android/onboarding/contracts/Launchable.kt (revision c625018464ae97c56936c82b1b617e11aa899faa)

<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