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