1 /* 2 * Copyright (C) 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 package com.android.server.appsearch.appsindexer; 17 18 import android.annotation.NonNull; 19 import android.app.appsearch.AppSearchSchema; 20 import android.app.appsearch.AppSearchSchema.DocumentPropertyConfig; 21 import android.app.appsearch.AppSearchSchema.PropertyConfig; 22 import android.app.appsearch.GenericDocument; 23 import android.app.appsearch.util.LogUtil; 24 import android.content.pm.PackageManager; 25 import android.content.res.AssetManager; 26 import android.util.ArrayMap; 27 import android.util.Log; 28 29 import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionDocument; 30 import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionStaticMetadata; 31 32 import org.xmlpull.v1.XmlPullParser; 33 import org.xmlpull.v1.XmlPullParserException; 34 import org.xmlpull.v1.XmlPullParserFactory; 35 36 import java.io.IOException; 37 import java.io.InputStreamReader; 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Objects; 43 44 /** 45 * This class parses static metadata about App Functions from an XML file located within an app's 46 * assets. 47 */ 48 public class AppFunctionDocumentParserImpl implements AppFunctionDocumentParser { 49 private static final String TAG = "AppSearchMetadataParser"; 50 private static final String XML_TAG_APPFUNCTION = "appfunction"; 51 private static final String XML_TAG_APPFUNCTIONS_ROOT = "appfunctions"; 52 private static final String XML_TAG_ID = "id"; 53 private static final String SNAKE_CASE_SEPARATOR = "_"; 54 55 @NonNull private final String mIndexerPackageName; 56 private final int mMaxAppFunctions; 57 58 /** 59 * @param indexerPackageName the name of the package performing the indexing. This should be the 60 * same as the package running the apps indexer. 61 * @param config the app indexer config used to enforce various limits during parsing. 62 */ AppFunctionDocumentParserImpl( @onNull String indexerPackageName, AppsIndexerConfig config)63 public AppFunctionDocumentParserImpl( 64 @NonNull String indexerPackageName, AppsIndexerConfig config) { 65 mIndexerPackageName = Objects.requireNonNull(indexerPackageName); 66 mMaxAppFunctions = config.getMaxAppFunctionsPerPackage(); 67 } 68 69 // TODO(b/367410454): Remove this method once enable_apps_indexer_incremental_put flag is 70 // rolled out 71 @NonNull 72 @Override parse( @onNull PackageManager packageManager, @NonNull String packageName, @NonNull String assetFilePath)73 public List<AppFunctionStaticMetadata> parse( 74 @NonNull PackageManager packageManager, 75 @NonNull String packageName, 76 @NonNull String assetFilePath) { 77 Objects.requireNonNull(packageManager); 78 Objects.requireNonNull(packageName); 79 Objects.requireNonNull(assetFilePath); 80 try { 81 return parseAppFunctions( 82 initializeParser(packageManager, packageName, assetFilePath), packageName); 83 } catch (Exception ex) { 84 // The code parses an XML file from another app's assets, using a broad try-catch to 85 // handle potential errors since the XML structure might be unpredictable. 86 Log.e( 87 TAG, 88 String.format( 89 "Failed to parse XML from package '%s', asset file '%s'", 90 packageName, assetFilePath), 91 ex); 92 } 93 return Collections.emptyList(); 94 } 95 96 @NonNull 97 @Override parseIntoMap( @onNull PackageManager packageManager, @NonNull String packageName, @NonNull String assetFilePath)98 public Map<String, AppFunctionStaticMetadata> parseIntoMap( 99 @NonNull PackageManager packageManager, 100 @NonNull String packageName, 101 @NonNull String assetFilePath) { 102 Objects.requireNonNull(packageManager); 103 Objects.requireNonNull(packageName); 104 Objects.requireNonNull(assetFilePath); 105 try { 106 return parseAppFunctionsIntoMap( 107 initializeParser(packageManager, packageName, assetFilePath), packageName); 108 } catch (Exception ex) { 109 // The code parses an XML file from another app's assets, using a broad try-catch to 110 // handle potential errors since the XML structure might be unpredictable. 111 Log.e( 112 TAG, 113 String.format( 114 "Failed to parse XML from package '%s', asset file '%s'", 115 packageName, assetFilePath), 116 ex); 117 } 118 return Collections.emptyMap(); 119 } 120 121 /** 122 * Initializes an {@link XmlPullParser} to parse xml based on the packageName and assetFilePath. 123 */ 124 @NonNull initializeParser( @onNull PackageManager packageManager, @NonNull String packageName, @NonNull String assetFilePath)125 private XmlPullParser initializeParser( 126 @NonNull PackageManager packageManager, 127 @NonNull String packageName, 128 @NonNull String assetFilePath) 129 throws XmlPullParserException, PackageManager.NameNotFoundException, IOException { 130 Objects.requireNonNull(packageManager); 131 Objects.requireNonNull(packageName); 132 Objects.requireNonNull(assetFilePath); 133 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 134 factory.setNamespaceAware(true); 135 XmlPullParser parser = factory.newPullParser(); 136 AssetManager assetManager = 137 packageManager.getResourcesForApplication(packageName).getAssets(); 138 parser.setInput(new InputStreamReader(assetManager.open(assetFilePath))); 139 return parser; 140 } 141 142 // TODO(b/367410454): Remove this method once enable_apps_indexer_incremental_put flag is 143 // rolled out 144 /** 145 * Parses a sequence of `appfunction` elements from the XML into a list of {@link 146 * AppFunctionStaticMetadata}. 147 * 148 * @param parser the XmlPullParser positioned at the start of the xml file 149 */ 150 @NonNull parseAppFunctions( @onNull XmlPullParser parser, @NonNull String packageName)151 private List<AppFunctionStaticMetadata> parseAppFunctions( 152 @NonNull XmlPullParser parser, @NonNull String packageName) 153 throws XmlPullParserException, IOException { 154 List<AppFunctionStaticMetadata> appFunctions = new ArrayList<>(); 155 156 int eventType = parser.getEventType(); 157 158 while (eventType != XmlPullParser.END_DOCUMENT) { 159 String tagName = parser.getName(); 160 if (eventType == XmlPullParser.START_TAG && XML_TAG_APPFUNCTION.equals(tagName)) { 161 AppFunctionStaticMetadata appFunction = parseAppFunction(parser, packageName); 162 appFunctions.add(appFunction); 163 if (appFunctions.size() >= mMaxAppFunctions) { 164 Log.d(TAG, "Exceeding the max number of app functions: " + packageName); 165 return appFunctions; 166 } 167 } 168 eventType = parser.next(); 169 } 170 return appFunctions; 171 } 172 173 /** 174 * Parses a sequence of `appfunction` elements from the XML into a map of function ids to their 175 * corresponding {@link AppFunctionStaticMetadata}. 176 * 177 * @param parser the XmlPullParser positioned at the start of the xml file 178 */ 179 @NonNull parseAppFunctionsIntoMap( @onNull XmlPullParser parser, @NonNull String packageName)180 private Map<String, AppFunctionStaticMetadata> parseAppFunctionsIntoMap( 181 @NonNull XmlPullParser parser, @NonNull String packageName) 182 throws XmlPullParserException, IOException { 183 Map<String, AppFunctionStaticMetadata> appFunctions = new ArrayMap<>(); 184 185 int eventType = parser.getEventType(); 186 187 while (eventType != XmlPullParser.END_DOCUMENT) { 188 String tagName = parser.getName(); 189 if (eventType == XmlPullParser.START_TAG && XML_TAG_APPFUNCTION.equals(tagName)) { 190 AppFunctionStaticMetadata appFunction = parseAppFunction(parser, packageName); 191 appFunctions.put(appFunction.getId(), appFunction); 192 if (appFunctions.size() >= mMaxAppFunctions) { 193 Log.d(TAG, "Exceeding the max number of app functions: " + packageName); 194 return appFunctions; 195 } 196 } 197 eventType = parser.next(); 198 } 199 return appFunctions; 200 } 201 202 /** 203 * Parses a single `appfunction` element from the XML into an {@link AppFunctionStaticMetadata} 204 * object. 205 * 206 * @param parser the XmlPullParser positioned at the start of an `appfunction` element. 207 * @return an AppFunction object populated with the data from the XML. 208 */ 209 @NonNull parseAppFunction( @onNull XmlPullParser parser, @NonNull String packageName)210 private AppFunctionStaticMetadata parseAppFunction( 211 @NonNull XmlPullParser parser, @NonNull String packageName) 212 throws XmlPullParserException, IOException { 213 String functionId = null; 214 String schemaName = null; 215 Long schemaVersion = null; 216 String schemaCategory = null; 217 Boolean enabledByDefault = null; 218 Integer displayNameStringRes = null; 219 Boolean restrictCallersWithExecuteAppFunctions = null; 220 int eventType = parser.getEventType(); 221 while (!(eventType == XmlPullParser.END_TAG 222 && XML_TAG_APPFUNCTION.equals(parser.getName()))) { 223 if (eventType == XmlPullParser.START_TAG 224 && !XML_TAG_APPFUNCTION.equals(parser.getName())) { 225 String tagName = parser.getName(); 226 switch (tagName) { 227 case "function_id": 228 functionId = parser.nextText().trim(); 229 break; 230 case "schema_name": 231 schemaName = parser.nextText().trim(); 232 break; 233 case "schema_version": 234 schemaVersion = Long.parseLong(parser.nextText().trim()); 235 break; 236 case "schema_category": 237 schemaCategory = parser.nextText().trim(); 238 break; 239 case "enabled_by_default": 240 enabledByDefault = Boolean.parseBoolean(parser.nextText().trim()); 241 break; 242 case "restrict_callers_with_execute_app_functions": 243 restrictCallersWithExecuteAppFunctions = 244 Boolean.parseBoolean(parser.nextText().trim()); 245 break; 246 case "display_name_string_res": 247 displayNameStringRes = Integer.parseInt(parser.nextText().trim()); 248 break; 249 } 250 } 251 eventType = parser.next(); 252 } 253 254 if (functionId == null) { 255 throw new XmlPullParserException("parseAppFunction: Missing functionId in the xml."); 256 } 257 AppFunctionStaticMetadata.Builder builder = 258 new AppFunctionStaticMetadata.Builder(packageName, functionId, mIndexerPackageName); 259 if (schemaName != null) { 260 builder.setSchemaName(schemaName); 261 } 262 if (schemaVersion != null) { 263 builder.setSchemaVersion(schemaVersion); 264 } 265 if (schemaCategory != null) { 266 builder.setSchemaCategory(schemaCategory); 267 } 268 if (enabledByDefault != null) { 269 builder.setEnabledByDefault(enabledByDefault); 270 } 271 if (restrictCallersWithExecuteAppFunctions != null) { 272 builder.setRestrictCallersWithExecuteAppFunctions( 273 restrictCallersWithExecuteAppFunctions); 274 } 275 if (displayNameStringRes != null) { 276 builder.setDisplayNameStringRes(displayNameStringRes); 277 } 278 return builder.build(); 279 } 280 281 @NonNull 282 @Override parseIntoMapForGivenSchemas( @onNull PackageManager packageManager, @NonNull String packageName, @NonNull String assetFilePath, @NonNull Map<String, AppSearchSchema> schemas)283 public Map<String, AppFunctionDocument> parseIntoMapForGivenSchemas( 284 @NonNull PackageManager packageManager, 285 @NonNull String packageName, 286 @NonNull String assetFilePath, 287 @NonNull Map<String, AppSearchSchema> schemas) { 288 Objects.requireNonNull(packageManager); 289 Objects.requireNonNull(packageName); 290 Objects.requireNonNull(assetFilePath); 291 Objects.requireNonNull(schemas); 292 293 try { 294 return parseAppFunctionsIntoMapForGivenSchemas( 295 initializeParser(packageManager, packageName, assetFilePath), 296 packageName, 297 schemas); 298 } catch (Exception ex) { 299 // The code parses an XML file from another app's assets, using a broad try-catch to 300 // handle potential errors since the XML structure might be unpredictable. 301 Log.e( 302 TAG, 303 String.format( 304 "Failed to parse XML from package '%s', asset file '%s'", 305 packageName, assetFilePath), 306 ex); 307 } 308 return Collections.emptyMap(); 309 } 310 311 @NonNull parseAppFunctionsIntoMapForGivenSchemas( @onNull XmlPullParser parser, @NonNull String packageName, @NonNull Map<String, AppSearchSchema> schemas)312 private Map<String, AppFunctionDocument> parseAppFunctionsIntoMapForGivenSchemas( 313 @NonNull XmlPullParser parser, 314 @NonNull String packageName, 315 @NonNull Map<String, AppSearchSchema> schemas) 316 throws XmlPullParserException, IOException { 317 Objects.requireNonNull(parser); 318 Objects.requireNonNull(packageName); 319 Objects.requireNonNull(schemas); 320 321 Map<String, AppFunctionDocument> appFnMetadatas = new ArrayMap<>(); 322 323 Map<String, PropertyConfig> qualifiedPropertyNamesToPropertyConfig = 324 buildQualifiedPropertyNameToPropertyConfigMap(schemas); 325 326 int eventType = parser.getEventType(); 327 328 while (eventType != XmlPullParser.END_DOCUMENT) { 329 String tagName = parser.getName(); 330 // In previous document formats <appfunction> XML tag was used for denoting 331 // AppFunctionStaticMetadata type. 332 String schemaType = 333 XML_TAG_APPFUNCTION.equals(tagName) 334 ? AppFunctionStaticMetadata.SCHEMA_TYPE 335 : tagName; 336 String schemaNameForPackage = 337 AppFunctionDocument.getSchemaNameForPackage(packageName, schemaType); 338 if (eventType == XmlPullParser.START_TAG && schemas.containsKey(schemaNameForPackage)) { 339 // Id of the document will be set after parsing the value from xml. 340 AppFunctionDocument.Builder appFnDocBuilder = 341 new AppFunctionDocument.Builder( 342 packageName, "", mIndexerPackageName, schemaType); 343 buildGenericDocumentFromXmlElement( 344 parser, 345 packageName, 346 schemaNameForPackage, 347 qualifiedPropertyNamesToPropertyConfig, 348 appFnDocBuilder); 349 350 AppFunctionDocument appFunctionDocument = appFnDocBuilder.build(); 351 appFnMetadatas.put(appFunctionDocument.getId(), appFunctionDocument); 352 if (appFnMetadatas.size() >= mMaxAppFunctions) { 353 if (LogUtil.DEBUG) { 354 Log.d(TAG, "Exceeding the max number of app functions: " + packageName); 355 } 356 return appFnMetadatas; 357 } 358 } 359 eventType = parser.next(); 360 } 361 return appFnMetadatas; 362 } 363 364 /** 365 * Tries to parse a single XML element and populate the {@link GenericDocument.Builder} object 366 * recursively. 367 * 368 * <p>When this function is called the parser should point to the xml element that marks the 369 * beginning of the {@link GenericDocument}, and would point to the end tag of the corresponding 370 * doc once this function completes. 371 * 372 * @param parser the XmlPullParser positioned at the start of an XML element. 373 * @param packageName the package name of the app that owns the XML element. 374 * @param schemaType the type of the schema that the XML element belongs to. 375 * @param qualifiedPropertyNamesToPropertyConfig the mapping of qualified property names to 376 * their corresponding {@link PropertyConfig} objects. 377 * @param docBuilder {@link GenericDocument.Builder} object to populate with the data from the 378 * XML element. 379 * @throws XmlPullParserException if the XML element is malformed. 380 */ buildGenericDocumentFromXmlElement( @onNull XmlPullParser parser, @NonNull String packageName, @NonNull String schemaType, @NonNull Map<String, PropertyConfig> qualifiedPropertyNamesToPropertyConfig, @NonNull GenericDocument.Builder docBuilder)381 private static void buildGenericDocumentFromXmlElement( 382 @NonNull XmlPullParser parser, 383 @NonNull String packageName, 384 @NonNull String schemaType, 385 @NonNull Map<String, PropertyConfig> qualifiedPropertyNamesToPropertyConfig, 386 @NonNull GenericDocument.Builder docBuilder) 387 throws XmlPullParserException, IOException { 388 Objects.requireNonNull(parser); 389 Objects.requireNonNull(packageName); 390 Objects.requireNonNull(qualifiedPropertyNamesToPropertyConfig); 391 392 Map<String, List<String>> primitivePropertyValues = new ArrayMap<>(); 393 Map<String, List<GenericDocument>> nestedDocumentValues = new ArrayMap<>(); 394 String startTag = parser.getName(); 395 String currentPropertyPath; 396 boolean wasDocIdSet = false; 397 398 // Skip the current tag that marks the beginning of the current document. 399 parser.next(); 400 401 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 402 switch (parser.getEventType()) { 403 case XmlPullParser.START_TAG: 404 currentPropertyPath = 405 createQualifiedPropertyName( 406 schemaType, 407 toLowerCamelCase(parser.getName(), SNAKE_CASE_SEPARATOR)); 408 PropertyConfig propertyConfig = 409 qualifiedPropertyNamesToPropertyConfig.get(currentPropertyPath); 410 if (propertyConfig instanceof DocumentPropertyConfig) { 411 String nestedSchemaType = 412 ((DocumentPropertyConfig) propertyConfig).getSchemaType(); 413 GenericDocument.Builder nestedDoc = 414 new GenericDocument.Builder( 415 AppFunctionStaticMetadata.APP_FUNCTION_NAMESPACE, 416 "", 417 nestedSchemaType); 418 buildGenericDocumentFromXmlElement( 419 parser, 420 packageName, 421 nestedSchemaType, 422 qualifiedPropertyNamesToPropertyConfig, 423 nestedDoc); 424 nestedDocumentValues 425 .computeIfAbsent(currentPropertyPath, k -> new ArrayList<>()) 426 .add(nestedDoc.build()); 427 } else if (propertyConfig != null) { 428 primitivePropertyValues 429 .computeIfAbsent(currentPropertyPath, k -> new ArrayList<>()) 430 .add(parser.nextText().trim()); 431 } else if (parser.getName().equals(XML_TAG_ID)) { 432 String id = parser.nextText().trim(); 433 if (!id.isEmpty()) { 434 docBuilder.setId(packageName + "/" + id); 435 wasDocIdSet = true; 436 } 437 } 438 break; 439 440 case XmlPullParser.END_TAG: 441 if (startTag.equals(parser.getName())) { 442 for (Map.Entry<String, List<String>> entry : 443 primitivePropertyValues.entrySet()) { 444 addPrimitiveProperty( 445 docBuilder, 446 qualifiedPropertyNamesToPropertyConfig.get(entry.getKey()), 447 entry.getValue()); 448 } 449 for (Map.Entry<String, List<GenericDocument>> entry : 450 nestedDocumentValues.entrySet()) { 451 String propertyName = 452 qualifiedPropertyNamesToPropertyConfig 453 .get(entry.getKey()) 454 .getName(); 455 docBuilder.setPropertyDocument( 456 propertyName, entry.getValue().toArray(new GenericDocument[0])); 457 } 458 if (!wasDocIdSet) { 459 throw new XmlPullParserException( 460 "No id found for document of type: " + schemaType); 461 } 462 return; 463 } 464 break; 465 } 466 parser.next(); 467 } 468 469 throw new IllegalStateException("Code should never reach here."); 470 } 471 472 /** 473 * Builds a mapping of qualified property names to their corresponding {@link PropertyConfig} 474 * objects. 475 * 476 * <p>The key is a concatenation of enclosing schema type and property name, separated by a 477 * period to avoid conflicts between properties with the same name in different schemas. For 478 * example, if the "Person" and "Address" schemas both have a property named "name", then the 479 * qualified property names will be "Person#name" and "Address#name" respectively. 480 * 481 * @param schemaMap the mapping of schema types to their corresponding {@link AppSearchSchema} 482 * objects. 483 * @return a {@link Map} of qualified property names to their corresponding {@link 484 * PropertyConfig} objects. 485 */ 486 @NonNull buildQualifiedPropertyNameToPropertyConfigMap( @onNull Map<String, AppSearchSchema> schemaMap)487 private static Map<String, PropertyConfig> buildQualifiedPropertyNameToPropertyConfigMap( 488 @NonNull Map<String, AppSearchSchema> schemaMap) { 489 Objects.requireNonNull(schemaMap); 490 491 Map<String, PropertyConfig> propertyMap = new ArrayMap<>(); 492 493 for (Map.Entry<String, AppSearchSchema> entry : schemaMap.entrySet()) { 494 String schemaType = entry.getKey(); 495 AppSearchSchema schema = entry.getValue(); 496 497 List<AppSearchSchema.PropertyConfig> properties = schema.getProperties(); 498 for (int i = 0; i < properties.size(); i++) { 499 AppSearchSchema.PropertyConfig property = properties.get(i); 500 String propertyPath = createQualifiedPropertyName(schemaType, property.getName()); 501 propertyMap.put(propertyPath, property); 502 } 503 } 504 505 return propertyMap; 506 } 507 508 /** 509 * Converts a string of words separated by separator to lowerCamelCase. 510 * 511 * <p>Returns the same string if string doesn't contain the separator. 512 */ toLowerCamelCase(@onNull String str, @NonNull String separator)513 private static String toLowerCamelCase(@NonNull String str, @NonNull String separator) { 514 if (str.isEmpty()) { 515 return ""; 516 } 517 518 // Return the original string if the separator is not present 519 if (!str.contains(separator)) { 520 return str; 521 } 522 523 StringBuilder builder = new StringBuilder(str.length()); 524 boolean capitalizeNext = false; 525 526 for (int i = 0; i < str.length(); i++) { 527 char currentChar = str.charAt(i); 528 // skip multiple consecutive separators 529 if (str.startsWith(separator, i)) { 530 capitalizeNext = true; 531 i += separator.length() - 1; 532 } else { 533 if (capitalizeNext) { 534 builder.append(Character.toUpperCase(currentChar)); 535 capitalizeNext = false; 536 } else { 537 builder.append(Character.toLowerCase(currentChar)); 538 } 539 } 540 } 541 542 return builder.toString(); 543 } 544 545 /** 546 * Creates a qualified property name by concatenating the schema type and property name with a # 547 * separator to avoid conflicts between properties with the same name in different schemas. 548 */ 549 @NonNull createQualifiedPropertyName( @onNull String schemaType, @NonNull String propertyName)550 private static String createQualifiedPropertyName( 551 @NonNull String schemaType, @NonNull String propertyName) { 552 return Objects.requireNonNull(schemaType) + "#" + Objects.requireNonNull(propertyName); 553 } 554 555 /** 556 * Adds primitive property values to the given {@link GenericDocument.Builder} based on the 557 * given {@link PropertyConfig}. 558 * 559 * <p>Ignores unsupported data types. 560 */ addPrimitiveProperty( @onNull GenericDocument.Builder builder, @NonNull PropertyConfig propertyConfig, @NonNull List<String> values)561 private static void addPrimitiveProperty( 562 @NonNull GenericDocument.Builder builder, 563 @NonNull PropertyConfig propertyConfig, 564 @NonNull List<String> values) { 565 Objects.requireNonNull(builder); 566 Objects.requireNonNull(propertyConfig); 567 Objects.requireNonNull(values); 568 569 switch (propertyConfig.getDataType()) { 570 case PropertyConfig.DATA_TYPE_BOOLEAN: 571 boolean[] booleanValues = new boolean[values.size()]; 572 for (int i = 0; i < values.size(); i++) { 573 booleanValues[i] = Boolean.parseBoolean(values.get(i)); 574 } 575 builder.setPropertyBoolean(propertyConfig.getName(), booleanValues); 576 break; 577 case PropertyConfig.DATA_TYPE_LONG: 578 long[] longValues = new long[values.size()]; 579 for (int i = 0; i < values.size(); i++) { 580 longValues[i] = Long.parseLong(values.get(i)); 581 } 582 builder.setPropertyLong(propertyConfig.getName(), longValues); 583 break; 584 case PropertyConfig.DATA_TYPE_STRING: 585 builder.setPropertyString(propertyConfig.getName(), values.toArray(new String[0])); 586 break; 587 default: 588 // fall-through 589 } 590 } 591 } 592