1 /*
2  * Copyright 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appfunctions
18 
19 import android.app.PendingIntent
20 import androidx.appfunctions.metadata.AppFunctionArrayTypeMetadata
21 import androidx.appfunctions.metadata.AppFunctionComponentsMetadata
22 import androidx.appfunctions.metadata.AppFunctionDataTypeMetadata
23 import androidx.appfunctions.metadata.AppFunctionObjectTypeMetadata
24 import androidx.appfunctions.metadata.AppFunctionParameterMetadata
25 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata
26 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata.Companion.TYPE_BOOLEAN
27 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata.Companion.TYPE_BYTES
28 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata.Companion.TYPE_DOUBLE
29 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata.Companion.TYPE_FLOAT
30 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata.Companion.TYPE_INT
31 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata.Companion.TYPE_LONG
32 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata.Companion.TYPE_PENDING_INTENT
33 import androidx.appfunctions.metadata.AppFunctionPrimitiveTypeMetadata.Companion.TYPE_STRING
34 import androidx.appfunctions.metadata.AppFunctionReferenceTypeMetadata
35 
36 /** Specification class defining the properties metadata for [AppFunctionData]. */
37 internal abstract class AppFunctionDataSpec {
38     abstract val objectQualifiedName: String
39     abstract val componentMetadata: AppFunctionComponentsMetadata
40 
getDataTypenull41     internal abstract fun getDataType(key: String): AppFunctionDataTypeMetadata?
42 
43     internal abstract fun isRequired(key: String): Boolean
44 
45     /** Checks if there is a metadata for [key]. */
46     fun containsMetadata(key: String): Boolean {
47         return getDataType(key) != null
48     }
49 
50     /**
51      * Gets the property object spec associated with [key].
52      *
53      * If the property associated with [key] is an Array, it would return the item object's
54      * specification.
55      *
56      * @throws IllegalArgumentException If this is no child specification associated with [key].
57      */
getPropertyObjectSpecnull58     fun getPropertyObjectSpec(key: String): AppFunctionDataSpec {
59         val childDataType =
60             getDataType(key)
61                 ?: throw IllegalArgumentException("Value associated with $key is not an object")
62         return when (childDataType) {
63             is AppFunctionArrayTypeMetadata -> {
64                 val itemObjectType =
65                     childDataType.itemType as? AppFunctionObjectTypeMetadata
66                         ?: throw IllegalArgumentException(
67                             "Value associated with $key is not an object array"
68                         )
69                 ObjectSpec(itemObjectType, componentMetadata)
70             }
71             is AppFunctionObjectTypeMetadata -> {
72                 ObjectSpec(childDataType, componentMetadata)
73             }
74             is AppFunctionReferenceTypeMetadata -> {
75                 val resolvedDataType = componentMetadata.dataTypes[childDataType.referenceDataType]
76                 if (
77                     resolvedDataType == null || resolvedDataType !is AppFunctionObjectTypeMetadata
78                 ) {
79                     throw IllegalArgumentException("Value associated with $key is not an object")
80                 }
81                 ObjectSpec(resolvedDataType, componentMetadata)
82             }
83             else -> {
84                 throw IllegalStateException("Unexpected data type $childDataType")
85             }
86         }
87     }
88 
89     /**
90      * Validates if [data] matches the current [AppFunctionDataSpec].
91      *
92      * @throws IllegalArgumentException If the [data] does not match the specification.
93      */
validateDataSpecMatchesnull94     fun validateDataSpecMatches(data: AppFunctionData) {
95         val otherSpec = data.spec ?: return
96         require(this == otherSpec) { "$data does not match the metadata specification of $this" }
97     }
98 
99     /**
100      * Validates if a write request to set a value of type [targetClass] to [targetKey] is valid.
101      *
102      * @param isCollection Indicates if the write request is a collection of [targetClass].
103      * @throws IllegalArgumentException If the request is invalid.
104      */
validateWriteRequestnull105     fun validateWriteRequest(
106         targetKey: String,
107         targetClass: Class<*>,
108         isCollection: Boolean,
109     ) {
110         val targetDataTypeMetadata = getDataType(targetKey)
111         if (targetDataTypeMetadata == null) {
112             throw IllegalArgumentException("No value should be set at $targetKey")
113         }
114         require(targetDataTypeMetadata.conform(targetClass, isCollection)) {
115             if (isCollection) {
116                 "Invalid value for $targetKey: got collection of $targetClass, " +
117                     "expecting a value matching $targetDataTypeMetadata"
118             } else {
119                 "Invalid value for $targetKey: got $targetClass, " +
120                     "expecting a value matching $targetDataTypeMetadata"
121             }
122         }
123     }
124 
125     /**
126      * Validates if a read request to get a value of type [targetClass] from [targetKey] is valid.
127      *
128      * @param isCollection Indicates if the write request is a collection of [targetClass].
129      * @throws IllegalArgumentException If the request is invalid.
130      */
validateReadRequestnull131     fun validateReadRequest(
132         targetKey: String,
133         targetClass: Class<*>,
134         isCollection: Boolean,
135     ) {
136         val targetDataTypeMetadata = getDataType(targetKey)
137         if (targetDataTypeMetadata == null) {
138             throw IllegalArgumentException("No value should be set at $targetKey")
139         }
140         require(targetDataTypeMetadata.conform(targetClass, isCollection)) {
141             if (isCollection) {
142                 "Unexpected read for $targetKey: expecting collection of $targetClass, " +
143                     "the actual value should be $targetDataTypeMetadata"
144             } else {
145                 "Unexpected read for $targetKey: expecting $targetClass, " +
146                     "the actual value should be $targetDataTypeMetadata"
147             }
148         }
149     }
150 
151     private data class ObjectSpec(
152         private val objectTypeMetadata: AppFunctionObjectTypeMetadata,
153         override val componentMetadata: AppFunctionComponentsMetadata
154     ) : AppFunctionDataSpec() {
155         override val objectQualifiedName: String
156             get() = objectTypeMetadata.qualifiedName ?: ""
157 
getDataTypenull158         override fun getDataType(key: String): AppFunctionDataTypeMetadata? {
159             return objectTypeMetadata.properties[key]
160         }
161 
isRequirednull162         override fun isRequired(key: String): Boolean {
163             return objectTypeMetadata.required.contains(key)
164         }
165     }
166 
167     private data class ParametersSpec(
168         private val parameterMetadataList: List<AppFunctionParameterMetadata>,
169         override val componentMetadata: AppFunctionComponentsMetadata
170     ) : AppFunctionDataSpec() {
171         override val objectQualifiedName: String
172             get() = ""
173 
getDataTypenull174         override fun getDataType(key: String): AppFunctionDataTypeMetadata? {
175             return parameterMetadataList.firstOrNull { it.name == key }?.dataType
176         }
177 
isRequirednull178         override fun isRequired(key: String): Boolean {
179             return parameterMetadataList.firstOrNull { it.name == key }?.isRequired ?: false
180         }
181     }
182 
AppFunctionDataTypeMetadatanull183     fun AppFunctionDataTypeMetadata.conform(typeClazz: Class<*>, isCollection: Boolean): Boolean {
184         return when (this) {
185             is AppFunctionPrimitiveTypeMetadata -> {
186                 isCollection == false && this.conform(typeClazz)
187             }
188             is AppFunctionArrayTypeMetadata -> {
189                 isCollection == true && this.conform(typeClazz)
190             }
191             is AppFunctionObjectTypeMetadata -> {
192                 isCollection == false && this.conform(typeClazz)
193             }
194             is AppFunctionReferenceTypeMetadata -> {
195                 isCollection == false && this.conform(typeClazz)
196             }
197             else -> {
198                 throw IllegalStateException("Unexpected data type ${this.javaClass}")
199             }
200         }
201     }
202 
conformnull203     private fun AppFunctionPrimitiveTypeMetadata.conform(typeClazz: Class<*>): Boolean {
204         return when (typeClazz) {
205             Int::class.java -> {
206                 this.type == TYPE_INT
207             }
208             Long::class.java -> {
209                 this.type == TYPE_LONG
210             }
211             Float::class.java -> {
212                 this.type == TYPE_FLOAT
213             }
214             Double::class.java -> {
215                 this.type == TYPE_DOUBLE
216             }
217             Boolean::class.java -> {
218                 this.type == TYPE_BOOLEAN
219             }
220             String::class.java -> {
221                 this.type == TYPE_STRING
222             }
223             Byte::class.java -> {
224                 this.type == TYPE_BYTES
225             }
226             PendingIntent::class.java -> {
227                 this.type == TYPE_PENDING_INTENT
228             }
229             else -> {
230                 false
231             }
232         }
233     }
234 
AppFunctionArrayTypeMetadatanull235     private fun AppFunctionArrayTypeMetadata.conform(itemTypeClass: Class<*>): Boolean {
236         return this.itemType.conform(itemTypeClass, isCollection = false)
237     }
238 
conformnull239     private fun AppFunctionObjectTypeMetadata.conform(typeClass: Class<*>): Boolean {
240         return typeClass == AppFunctionData::class.java
241     }
242 
conformnull243     private fun AppFunctionReferenceTypeMetadata.conform(typeClass: Class<*>): Boolean {
244         // Reference Type is always an object type
245         return typeClass == AppFunctionData::class.java
246     }
247 
248     companion object {
createnull249         fun create(
250             objectType: AppFunctionObjectTypeMetadata,
251             componentMetadata: AppFunctionComponentsMetadata
252         ): AppFunctionDataSpec {
253             return ObjectSpec(objectType, componentMetadata)
254         }
255 
createnull256         fun create(
257             parameterMetadataList: List<AppFunctionParameterMetadata>,
258             componentMetadata: AppFunctionComponentsMetadata
259         ): AppFunctionDataSpec {
260             return ParametersSpec(parameterMetadataList, componentMetadata)
261         }
262     }
263 }
264