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