1 package com.android.onboarding.contracts.annotations
2 
3 /** Container of metadata about a node definition in the onboarding graph */
4 @Retention(AnnotationRetention.RUNTIME)
5 annotation class OnboardingNode(
6   /** Identifier of the component who owns the node. */
7   val component: String,
8 
9   /**
10    * Identifier of the node interface.
11    *
12    * Note that this must be at most 40 characters long, otherwise an exception will be thrown.
13    */
14   val name: String,
15 
16   /** The type of UI shown as part of this node. */
17   val uiType: UiType,
18 
19   /** The type of specification given for this node. */
20   val specificationType: SpecificationType = SpecificationType.LIGHT,
21 
22   /** The packages which are expected to launch this node without making use of the contract. */
23   // Once we need it - we can add constants representing categories of caller that can be
24   // special cased. For example, DPCs or Settings apps.
25   val offGraphCallers: Array<String> = [],
26 
27   /**
28    * True if this node is expected to terminate the Onboarding flow in some circumstances.
29    *
30    * This is only applicable to UI Nodes.
31    *
32    * No node with this set to false should exit without returning a result or starting another
33    * contract which continues the flow.
34    */
35   // We should refactor this into the UiType option somehow so it can't be specified on non-UI nodes
36   // or at least we should enforce it in the linter
37   val isTerminalNode: Boolean = false,
38 
39   /**
40    * How the back button behaves when this node is active.
41    *
42    * This is only applicable to UI Nodes.
43    */
44   // We should refactor this into the UiType option somehow so it can't be specified on non-UI nodes
45   // or at least we should enforce it in the linter
46   val backButtonBehavior: BackButtonBehavior = BackButtonBehavior.UNKNOWN,
47 ) {
48 
49   enum class BackButtonBehavior {
50     UNKNOWN,
51 
52     /** Disabled via some screen-specific logic. */
53     CUSTOM_DISABLED,
54   }
55 
56   enum class UiType {
57     /** This node shows no UI. */
58     NONE,
59 
60     /** This node shows UI which does not fit any other type. */
61     OTHER,
62 
63     /**
64      * This node does not actually show UI but makes use of UI controls as a way of passing control.
65      * This will be replaced by some other mechanism in future.
66      */
67     INVISIBLE,
68 
69     /**
70      * This node shows a loading screen. The primary purpose is to control the screen while some
71      * background work executes.
72      */
73     LOADING,
74 
75     /**
76      * This node shows an education screen. The primary purpose is to educate the user about their
77      * device, privacy, etc.
78      */
79     EDUCATION,
80 
81     /** This node's primary purpose is to input a decision or some data from the user. */
82     INPUT,
83 
84     /**
85      * This node's primary purpose is to host some other UI node. This node must not show UI itself
86      * independently of another node.
87      *
88      * For example, an Activity which hosts fragments, where those fragments themselves are nodes,
89      * could be marked as [HOST].
90      */
91     HOST,
92 
93     /** This node's primary purpose is to show error screen to the user. */
94     ERROR,
95   }
96 
97   enum class SpecificationType {
98     /**
99      * No requirements.
100      *
101      * Most "Light" nodes will be lacking documentation, and using Intents and Bundles as arguments
102      * and return values.
103      */
104     LIGHT,
105 
106     /**
107      * This means that full Javadoc is provided, all arguments are fully defined, documented and
108      * typed (no undefined Bundles or Intents), and return values are properly defined and typed.
109      */
110     BASELINE,
111 
112     /**
113      * Same as [BASELINE] except that we have confidence in our contract completeness so can enforce
114      * strict validation
115      */
116     V1,
117   }
118 
119   companion object {
120     /** Returns the node [component] for given node's contract class. */
extractComponentNameFromClassnull121     fun extractComponentNameFromClass(nodeClass: Class<*>): String =
122       nodeClass.getAnnotation(OnboardingNode::class.java)?.component
123         ?: throw IllegalArgumentException("All nodes must be annotated @OnboardingNode")
124 
125     /** Returns the node [name] for given node's contract class. */
126     fun extractNodeNameFromClass(nodeClass: Class<*>): String =
127       nodeClass.getAnnotation(OnboardingNode::class.java)?.name?.also {
128         require(it.length <= MAX_NODE_NAME_LENGTH) {
129           "Node name length (${it.length}) exceeds maximum length of $MAX_NODE_NAME_LENGTH characters"
130         }
131       } ?: throw IllegalArgumentException("All nodes must be annotated @OnboardingNode")
132 
133     /** Returns the node [OnboardingNodeMetadata] for given node's component name. */
getOnboardingNodeMetadatanull134     fun getOnboardingNodeMetadata(nodeClass: Class<*>): OnboardingNodeMetadata {
135       val component =
136         nodeClass.getAnnotation(OnboardingNode::class.java)?.component
137           ?: error("OnboardingNode annotation or component is missing for class ${nodeClass.name}")
138       val nodeMetadata = component.split("/")
139       return when (nodeMetadata.size) {
140         1 -> OnboardingNodeMetadata(nodeMetadata[0], nodeMetadata[0])
141         2 -> OnboardingNodeMetadata(nodeMetadata[0], nodeMetadata[1])
142         else -> error("OnboardingNode component ${component} is invalid")
143       }
144     }
145   }
146 }
147 
148 const val MAX_NODE_NAME_LENGTH: Int = 40
149