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