xref: /aosp_15_r20/external/android_onboarding/java/com/android/onboarding/activity/OnboardingActivity.kt (revision c625018464ae97c56936c82b1b617e11aa899faa)
1 package com.android.onboarding.activity
2 
3 import android.app.Activity
4 import android.content.Intent
5 import android.os.Bundle
6 import androidx.appcompat.app.AppCompatActivity
7 import androidx.annotation.ContentView
8 import androidx.annotation.LayoutRes
9 import com.android.onboarding.contracts.AttachedResult
10 import com.android.onboarding.contracts.OnboardingActivityApiContract
11 import com.android.onboarding.contracts.annotations.DiscouragedOnboardingApi
12 import com.android.onboarding.contracts.annotations.InternalOnboardingApi
13 import com.android.onboarding.contracts.annotations.OnboardingNode
14 import com.android.onboarding.contracts.registerForActivityLaunch
15 import kotlin.properties.Delegates
16 
17 /** An activity base for onboarding components linking them directly to a given [contract]. */
18 abstract class OnboardingActivity<I : Any, O : Any, C : OnboardingActivityApiContract<I, O>> :
19   AppCompatActivity {
20   constructor() : super()
21 
22   @ContentView
23   constructor(@LayoutRes contentLayoutId: Int) : super(contentLayoutId)
24 
25   /**
26    * [OnboardingActivityApiContract] bound to this activity.
27    *
28    * If injected lazily, implementations
29    * must ensure that it is available before any calls to [onCreate].
30    */
31   protected abstract val contract: C
32 
33   /**
34    * Argument extracted for this activity instance via the provided [contract].
35    *
36    * Accessing it before [onCreate] will result in an exception.
37    */
38   protected var argument: I by Delegates.notNull()
39     private set
40 
41   protected var attachResult: AttachedResult by Delegates.notNull()
42     private set
43 
44   /**
45    * Checks [OnboardingNode.specificationType] to determine if internal errors should be eager or
46    * lazy.
47    */
<lambda>null48   private val strict by lazy {
49     @OptIn(InternalOnboardingApi::class)
50     contract.metadata.specificationType > OnboardingNode.SpecificationType.BASELINE
51   }
52 
onCreatenull53   override fun onCreate(savedInstanceState: Bundle?) {
54     super.onCreate(savedInstanceState)
55     attachResult = contract.attach(this, rawIntent)
56     runCatching { contract.extractArgument(rawIntent) }
57       .onSuccess { argument = it }
58       .onFailure { if (strict) throw it }
59   }
60 
61   /**
62    * Sets the activity [result] via [contract].
63    *
64    * @param result typesafe representation of the activity result
65    */
setResultnull66   protected fun setResult(result: O) {
67     contract.setResult(this, result)
68   }
69 
70   /** Direct access to the [Activity.getIntent] bypassing all the checks. */
71   internal inline val rawIntent
72     get() = super.getIntent()
73 
callStrictApinull74   private inline fun <R> callStrictApi(method: String, call: () -> R): R =
75     if (strict) {
76       error(
77         "Calls to ::$method are forbidden for OnboardingNode.specificationType > OnboardingNode.SpecificationType.BASELINE"
78       )
79     } else {
80       call()
81     }
82 
83   /**
84    * Prefer [argument] instead.
85    */
86   @DiscouragedOnboardingApi("Prefer accessing intent data via extracted argument")
<lambda>null87   override fun getIntent(): Intent = callStrictApi("getIntent") { rawIntent }
88 
89   /**
90    * Prefer [registerForActivityLaunch] instead.
91    */
92   @DiscouragedOnboardingApi("Prefer launching activities via their contracts")
startActivitynull93   override fun startActivity(intent: Intent) =
94     callStrictApi("startActivity") { super.startActivity(intent) }
95 
96   /**
97    * Prefer [registerForActivityLaunch] instead.
98    */
99   @DiscouragedOnboardingApi("Prefer launching activities via their contracts")
startActivitynull100   override fun startActivity(intent: Intent, options: Bundle?) =
101     callStrictApi("startActivity") { super.startActivity(intent, options) }
102 }
103 
104 /**
105  * A specialised [OnboardingActivity] tailored for the (discouraged) cases where a single activity
106  * is fulfilling multiple contracts.
107  *
108  * An actual contract is selected during [onCreate] and cached for the entire lifecycle of the
109  * [Activity]. The expectation is that most of the consumers would be able to determine the contract
110  * by the [Intent] data.
111  */
112 @DiscouragedOnboardingApi("Migrate to OnboardingActivity")
113 abstract class CompositeOnboardingActivity<
114   I : Any,
115   O : Any,
116   C : OnboardingActivityApiContract<I, O>,
117   > : OnboardingActivity<I, O, C>() {
118   /** Selector for a contract [C]. Resolved once per activity lifecycle. */
selectContractnull119   protected abstract fun selectContract(intent: Intent): C
120 
121   final override val contract: C by lazy { selectContract(rawIntent) }
122 }
123