1 /* 2 * Copyright 2020 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 android.app.appsearch; 18 19 import android.annotation.IntDef; 20 import android.annotation.IntRange; 21 import android.annotation.NonNull; 22 import android.annotation.SuppressLint; 23 import android.app.appsearch.util.BundleUtil; 24 import android.os.Bundle; 25 import android.util.ArrayMap; 26 27 import com.android.internal.util.Preconditions; 28 29 import java.lang.annotation.Retention; 30 import java.lang.annotation.RetentionPolicy; 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.Collection; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Objects; 38 import java.util.Set; 39 40 /** 41 * This class represents the specification logic for AppSearch. It can be used to set the type of 42 * search, like prefix or exact only or apply filters to search for a specific schema type only etc. 43 */ 44 // TODO(sidchhabra) : AddResultSpec fields for Snippets etc. 45 public final class SearchSpec { 46 /** 47 * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply property paths to 48 * all results, excepting any types that have had their own, specific property paths set. 49 */ 50 public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*"; 51 52 static final String TERM_MATCH_TYPE_FIELD = "termMatchType"; 53 static final String SCHEMA_FIELD = "schema"; 54 static final String NAMESPACE_FIELD = "namespace"; 55 static final String PACKAGE_NAME_FIELD = "packageName"; 56 static final String NUM_PER_PAGE_FIELD = "numPerPage"; 57 static final String RANKING_STRATEGY_FIELD = "rankingStrategy"; 58 static final String ORDER_FIELD = "order"; 59 static final String SNIPPET_COUNT_FIELD = "snippetCount"; 60 static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty"; 61 static final String MAX_SNIPPET_FIELD = "maxSnippet"; 62 static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks"; 63 static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags"; 64 static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit"; 65 66 /** @hide */ 67 public static final int DEFAULT_NUM_PER_PAGE = 10; 68 69 // TODO(b/170371356): In framework, we may want these limits to be flag controlled. 70 // If that happens, the @IntRange() directives in this class may have to change. 71 private static final int MAX_NUM_PER_PAGE = 10_000; 72 private static final int MAX_SNIPPET_COUNT = 10_000; 73 private static final int MAX_SNIPPET_PER_PROPERTY_COUNT = 10_000; 74 private static final int MAX_SNIPPET_SIZE_LIMIT = 10_000; 75 76 /** 77 * Term Match Type for the query. 78 * 79 * @hide 80 */ 81 // NOTE: The integer values of these constants must match the proto enum constants in 82 // {@link com.google.android.icing.proto.SearchSpecProto.termMatchType} 83 @IntDef(value = {TERM_MATCH_EXACT_ONLY, TERM_MATCH_PREFIX}) 84 @Retention(RetentionPolicy.SOURCE) 85 public @interface TermMatch {} 86 87 /** 88 * Query terms will only match exact tokens in the index. 89 * 90 * <p>Ex. A query term "foo" will only match indexed token "foo", and not "foot" or "football". 91 */ 92 public static final int TERM_MATCH_EXACT_ONLY = 1; 93 /** 94 * Query terms will match indexed tokens when the query term is a prefix of the token. 95 * 96 * <p>Ex. A query term "foo" will match indexed tokens like "foo", "foot", and "football". 97 */ 98 public static final int TERM_MATCH_PREFIX = 2; 99 100 /** 101 * Ranking Strategy for query result. 102 * 103 * @hide 104 */ 105 // NOTE: The integer values of these constants must match the proto enum constants in 106 // {@link ScoringSpecProto.RankingStrategy.Code} 107 @IntDef( 108 value = { 109 RANKING_STRATEGY_NONE, 110 RANKING_STRATEGY_DOCUMENT_SCORE, 111 RANKING_STRATEGY_CREATION_TIMESTAMP, 112 RANKING_STRATEGY_RELEVANCE_SCORE, 113 RANKING_STRATEGY_USAGE_COUNT, 114 RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP, 115 RANKING_STRATEGY_SYSTEM_USAGE_COUNT, 116 RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP, 117 }) 118 @Retention(RetentionPolicy.SOURCE) 119 public @interface RankingStrategy {} 120 121 /** No Ranking, results are returned in arbitrary order. */ 122 public static final int RANKING_STRATEGY_NONE = 0; 123 /** Ranked by app-provided document scores. */ 124 public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; 125 /** Ranked by document creation timestamps. */ 126 public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; 127 /** Ranked by document relevance score. */ 128 public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; 129 /** Ranked by number of usages, as reported by the app. */ 130 public static final int RANKING_STRATEGY_USAGE_COUNT = 4; 131 /** Ranked by timestamp of last usage, as reported by the app. */ 132 public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; 133 /** Ranked by number of usages from a system UI surface. */ 134 public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6; 135 /** Ranked by timestamp of last usage from a system UI surface. */ 136 public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; 137 138 /** 139 * Order for query result. 140 * 141 * @hide 142 */ 143 // NOTE: The integer values of these constants must match the proto enum constants in 144 // {@link ScoringSpecProto.Order.Code} 145 @IntDef(value = {ORDER_DESCENDING, ORDER_ASCENDING}) 146 @Retention(RetentionPolicy.SOURCE) 147 public @interface Order {} 148 149 /** Search results will be returned in a descending order. */ 150 public static final int ORDER_DESCENDING = 0; 151 /** Search results will be returned in an ascending order. */ 152 public static final int ORDER_ASCENDING = 1; 153 154 /** 155 * Grouping type for result limits. 156 * 157 * @hide 158 */ 159 @IntDef( 160 flag = true, 161 value = {GROUPING_TYPE_PER_PACKAGE, GROUPING_TYPE_PER_NAMESPACE}) 162 @Retention(RetentionPolicy.SOURCE) 163 public @interface GroupingType {} 164 165 /** 166 * Results should be grouped together by package for the purpose of enforcing a limit on the 167 * number of results returned per package. 168 */ 169 public static final int GROUPING_TYPE_PER_PACKAGE = 0b01; 170 /** 171 * Results should be grouped together by namespace for the purpose of enforcing a limit on the 172 * number of results returned per namespace. 173 */ 174 public static final int GROUPING_TYPE_PER_NAMESPACE = 0b10; 175 176 private final Bundle mBundle; 177 178 /** @hide */ SearchSpec(@onNull Bundle bundle)179 public SearchSpec(@NonNull Bundle bundle) { 180 Objects.requireNonNull(bundle); 181 mBundle = bundle; 182 } 183 184 /** 185 * Returns the {@link Bundle} populated by this builder. 186 * 187 * @hide 188 */ 189 @NonNull getBundle()190 public Bundle getBundle() { 191 return mBundle; 192 } 193 194 /** Returns how the query terms should match terms in the index. */ getTermMatch()195 public @TermMatch int getTermMatch() { 196 return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1); 197 } 198 199 /** 200 * Returns the list of schema types to search for. 201 * 202 * <p>If empty, the query will search over all schema types. 203 */ 204 @NonNull getFilterSchemas()205 public List<String> getFilterSchemas() { 206 List<String> schemas = mBundle.getStringArrayList(SCHEMA_FIELD); 207 if (schemas == null) { 208 return Collections.emptyList(); 209 } 210 return Collections.unmodifiableList(schemas); 211 } 212 213 /** 214 * Returns the list of namespaces to search over. 215 * 216 * <p>If empty, the query will search over all namespaces. 217 */ 218 @NonNull getFilterNamespaces()219 public List<String> getFilterNamespaces() { 220 List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD); 221 if (namespaces == null) { 222 return Collections.emptyList(); 223 } 224 return Collections.unmodifiableList(namespaces); 225 } 226 227 /** 228 * Returns the list of package name filters to search over. 229 * 230 * <p>If empty, the query will search over all packages that the caller has access to. If 231 * package names are specified which caller doesn't have access to, then those package names 232 * will be ignored. 233 */ 234 @NonNull getFilterPackageNames()235 public List<String> getFilterPackageNames() { 236 List<String> packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD); 237 if (packageNames == null) { 238 return Collections.emptyList(); 239 } 240 return Collections.unmodifiableList(packageNames); 241 } 242 243 /** Returns the number of results per page in the result set. */ getResultCountPerPage()244 public int getResultCountPerPage() { 245 return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE); 246 } 247 248 /** Returns the ranking strategy. */ getRankingStrategy()249 public @RankingStrategy int getRankingStrategy() { 250 return mBundle.getInt(RANKING_STRATEGY_FIELD); 251 } 252 253 /** Returns the order of returned search results (descending or ascending). */ getOrder()254 public @Order int getOrder() { 255 return mBundle.getInt(ORDER_FIELD); 256 } 257 258 /** Returns how many documents to generate snippets for. */ getSnippetCount()259 public int getSnippetCount() { 260 return mBundle.getInt(SNIPPET_COUNT_FIELD); 261 } 262 263 /** 264 * Returns how many matches for each property of a matching document to generate snippets for. 265 */ getSnippetCountPerProperty()266 public int getSnippetCountPerProperty() { 267 return mBundle.getInt(SNIPPET_COUNT_PER_PROPERTY_FIELD); 268 } 269 270 /** Returns the maximum size of a snippet in characters. */ getMaxSnippetSize()271 public int getMaxSnippetSize() { 272 return mBundle.getInt(MAX_SNIPPET_FIELD); 273 } 274 275 /** 276 * Returns a map from schema type to property paths to be used for projection. 277 * 278 * <p>If the map is empty, then all properties will be retrieved for all results. 279 * 280 * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this 281 * function, rather than calling it multiple times. 282 */ 283 @NonNull getProjections()284 public Map<String, List<String>> getProjections() { 285 Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD); 286 Set<String> schemas = typePropertyPathsBundle.keySet(); 287 Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size()); 288 for (String schema : schemas) { 289 typePropertyPathsMap.put(schema, typePropertyPathsBundle.getStringArrayList(schema)); 290 } 291 return typePropertyPathsMap; 292 } 293 294 /** 295 * Get the type of grouping limit to apply, or 0 if {@link Builder#setResultGrouping} was not 296 * called. 297 */ getResultGroupingTypeFlags()298 public @GroupingType int getResultGroupingTypeFlags() { 299 return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS); 300 } 301 302 /** 303 * Get the maximum number of results to return for each group. 304 * 305 * @return the maximum number of results to return for each group or Integer.MAX_VALUE if {@link 306 * Builder#setResultGrouping(int, int)} was not called. 307 */ getResultGroupingLimit()308 public int getResultGroupingLimit() { 309 return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE); 310 } 311 312 /** Builder for {@link SearchSpec objects}. */ 313 public static final class Builder { 314 private ArrayList<String> mSchemas = new ArrayList<>(); 315 private ArrayList<String> mNamespaces = new ArrayList<>(); 316 private ArrayList<String> mPackageNames = new ArrayList<>(); 317 private Bundle mProjectionTypePropertyMasks = new Bundle(); 318 319 private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE; 320 private @TermMatch int mTermMatchType = TERM_MATCH_PREFIX; 321 private int mSnippetCount = 0; 322 private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT; 323 private int mMaxSnippetSize = 0; 324 private @RankingStrategy int mRankingStrategy = RANKING_STRATEGY_NONE; 325 private @Order int mOrder = ORDER_DESCENDING; 326 private @GroupingType int mGroupingTypeFlags = 0; 327 private int mGroupingLimit = 0; 328 private boolean mBuilt = false; 329 330 /** 331 * Indicates how the query terms should match {@code TermMatchCode} in the index. 332 * 333 * <p>If this method is not called, the default term match type is {@link 334 * SearchSpec#TERM_MATCH_PREFIX}. 335 */ 336 @NonNull setTermMatch(@ermMatch int termMatchType)337 public Builder setTermMatch(@TermMatch int termMatchType) { 338 Preconditions.checkArgumentInRange( 339 termMatchType, TERM_MATCH_EXACT_ONLY, TERM_MATCH_PREFIX, "Term match type"); 340 resetIfBuilt(); 341 mTermMatchType = termMatchType; 342 return this; 343 } 344 345 /** 346 * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that 347 * have the specified schema types. 348 * 349 * <p>If unset, the query will search over all schema types. 350 */ 351 @NonNull addFilterSchemas(@onNull String... schemas)352 public Builder addFilterSchemas(@NonNull String... schemas) { 353 Objects.requireNonNull(schemas); 354 resetIfBuilt(); 355 return addFilterSchemas(Arrays.asList(schemas)); 356 } 357 358 /** 359 * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that 360 * have the specified schema types. 361 * 362 * <p>If unset, the query will search over all schema types. 363 */ 364 @NonNull addFilterSchemas(@onNull Collection<String> schemas)365 public Builder addFilterSchemas(@NonNull Collection<String> schemas) { 366 Objects.requireNonNull(schemas); 367 resetIfBuilt(); 368 mSchemas.addAll(schemas); 369 return this; 370 } 371 372 /** 373 * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that have 374 * the specified namespaces. 375 * 376 * <p>If unset, the query will search over all namespaces. 377 */ 378 @NonNull addFilterNamespaces(@onNull String... namespaces)379 public Builder addFilterNamespaces(@NonNull String... namespaces) { 380 Objects.requireNonNull(namespaces); 381 resetIfBuilt(); 382 return addFilterNamespaces(Arrays.asList(namespaces)); 383 } 384 385 /** 386 * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that have 387 * the specified namespaces. 388 * 389 * <p>If unset, the query will search over all namespaces. 390 */ 391 @NonNull addFilterNamespaces(@onNull Collection<String> namespaces)392 public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) { 393 Objects.requireNonNull(namespaces); 394 resetIfBuilt(); 395 mNamespaces.addAll(namespaces); 396 return this; 397 } 398 399 /** 400 * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that 401 * were indexed from the specified packages. 402 * 403 * <p>If unset, the query will search over all packages that the caller has access to. If 404 * package names are specified which caller doesn't have access to, then those package names 405 * will be ignored. 406 */ 407 @NonNull addFilterPackageNames(@onNull String... packageNames)408 public Builder addFilterPackageNames(@NonNull String... packageNames) { 409 Objects.requireNonNull(packageNames); 410 resetIfBuilt(); 411 return addFilterPackageNames(Arrays.asList(packageNames)); 412 } 413 414 /** 415 * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that 416 * were indexed from the specified packages. 417 * 418 * <p>If unset, the query will search over all packages that the caller has access to. If 419 * package names are specified which caller doesn't have access to, then those package names 420 * will be ignored. 421 */ 422 @NonNull addFilterPackageNames(@onNull Collection<String> packageNames)423 public Builder addFilterPackageNames(@NonNull Collection<String> packageNames) { 424 Objects.requireNonNull(packageNames); 425 resetIfBuilt(); 426 mPackageNames.addAll(packageNames); 427 return this; 428 } 429 430 /** 431 * Sets the number of results per page in the returned object. 432 * 433 * <p>The default number of results per page is 10. 434 */ 435 @NonNull setResultCountPerPage( @ntRangefrom = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage)436 public SearchSpec.Builder setResultCountPerPage( 437 @IntRange(from = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage) { 438 Preconditions.checkArgumentInRange( 439 resultCountPerPage, 0, MAX_NUM_PER_PAGE, "resultCountPerPage"); 440 resetIfBuilt(); 441 mResultCountPerPage = resultCountPerPage; 442 return this; 443 } 444 445 /** Sets ranking strategy for AppSearch results. */ 446 @NonNull setRankingStrategy(@ankingStrategy int rankingStrategy)447 public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) { 448 Preconditions.checkArgumentInRange( 449 rankingStrategy, 450 RANKING_STRATEGY_NONE, 451 RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP, 452 "Result ranking strategy"); 453 resetIfBuilt(); 454 mRankingStrategy = rankingStrategy; 455 return this; 456 } 457 458 /** 459 * Indicates the order of returned search results, the default is {@link #ORDER_DESCENDING}, 460 * meaning that results with higher scores come first. 461 * 462 * <p>This order field will be ignored if RankingStrategy = {@code RANKING_STRATEGY_NONE}. 463 */ 464 @NonNull setOrder(@rder int order)465 public Builder setOrder(@Order int order) { 466 Preconditions.checkArgumentInRange( 467 order, ORDER_DESCENDING, ORDER_ASCENDING, "Result ranking order"); 468 resetIfBuilt(); 469 mOrder = order; 470 return this; 471 } 472 473 /** 474 * Only the first {@code snippetCount} documents based on the ranking strategy will have 475 * snippet information provided. 476 * 477 * <p>The list returned from {@link SearchResult#getMatchInfos} will contain at most this 478 * many entries. 479 * 480 * <p>If set to 0 (default), snippeting is disabled and the list returned from {@link 481 * SearchResult#getMatchInfos} will be empty. 482 */ 483 @NonNull setSnippetCount( @ntRangefrom = 0, to = MAX_SNIPPET_COUNT) int snippetCount)484 public SearchSpec.Builder setSnippetCount( 485 @IntRange(from = 0, to = MAX_SNIPPET_COUNT) int snippetCount) { 486 Preconditions.checkArgumentInRange(snippetCount, 0, MAX_SNIPPET_COUNT, "snippetCount"); 487 resetIfBuilt(); 488 mSnippetCount = snippetCount; 489 return this; 490 } 491 492 /** 493 * Sets {@code snippetCountPerProperty}. Only the first {@code snippetCountPerProperty} 494 * snippets for each property of each {@link GenericDocument} will contain snippet 495 * information. 496 * 497 * <p>If set to 0, snippeting is disabled and the list returned from {@link 498 * SearchResult#getMatchInfos} will be empty. 499 * 500 * <p>The default behavior is to snippet all matches a property contains, up to the maximum 501 * value of 10,000. 502 */ 503 @NonNull setSnippetCountPerProperty( @ntRangefrom = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT) int snippetCountPerProperty)504 public SearchSpec.Builder setSnippetCountPerProperty( 505 @IntRange(from = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT) 506 int snippetCountPerProperty) { 507 Preconditions.checkArgumentInRange( 508 snippetCountPerProperty, 509 0, 510 MAX_SNIPPET_PER_PROPERTY_COUNT, 511 "snippetCountPerProperty"); 512 resetIfBuilt(); 513 mSnippetCountPerProperty = snippetCountPerProperty; 514 return this; 515 } 516 517 /** 518 * Sets {@code maxSnippetSize}, the maximum snippet size. Snippet windows start at {@code 519 * maxSnippetSize/2} bytes before the middle of the matching token and end at {@code 520 * maxSnippetSize/2} bytes after the middle of the matching token. It respects token 521 * boundaries, therefore the returned window may be smaller than requested. 522 * 523 * <p>Setting {@code maxSnippetSize} to 0 will disable windowing and an empty string will be 524 * returned. If matches enabled is also set to false, then snippeting is disabled. 525 * 526 * <p>Ex. {@code maxSnippetSize} = 16. "foo bar baz bat rat" with a query of "baz" will 527 * return a window of "bar baz bat" which is only 11 bytes long. 528 */ 529 @NonNull setMaxSnippetSize( @ntRangefrom = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize)530 public SearchSpec.Builder setMaxSnippetSize( 531 @IntRange(from = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize) { 532 Preconditions.checkArgumentInRange( 533 maxSnippetSize, 0, MAX_SNIPPET_SIZE_LIMIT, "maxSnippetSize"); 534 resetIfBuilt(); 535 mMaxSnippetSize = maxSnippetSize; 536 return this; 537 } 538 539 /** 540 * Adds property paths for the specified type to be used for projection. If property paths 541 * are added for a type, then only the properties referred to will be retrieved for results 542 * of that type. If a property path that is specified isn't present in a result, it will be 543 * ignored for that result. Property paths cannot be null. 544 * 545 * <p>If no property paths are added for a particular type, then all properties of results 546 * of that type will be retrieved. 547 * 548 * <p>If property path is added for the {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, 549 * then those property paths will apply to all results, excepting any types that have their 550 * own, specific property paths set. 551 * 552 * <p>Suppose the following document is in the index. 553 * 554 * <pre>{@code 555 * Email: Document { 556 * sender: Document { 557 * name: "Mr. Person" 558 * email: "mrperson123@google.com" 559 * } 560 * recipients: [ 561 * Document { 562 * name: "John Doe" 563 * email: "johndoe123@google.com" 564 * } 565 * Document { 566 * name: "Jane Doe" 567 * email: "janedoe123@google.com" 568 * } 569 * ] 570 * subject: "IMPORTANT" 571 * body: "Limited time offer!" 572 * } 573 * }</pre> 574 * 575 * <p>Then, suppose that a query for "important" is issued with the following projection 576 * type property paths: 577 * 578 * <pre>{@code 579 * {schema: "Email", ["subject", "sender.name", "recipients.name"]} 580 * }</pre> 581 * 582 * <p>The above document will be returned as: 583 * 584 * <pre>{@code 585 * Email: Document { 586 * sender: Document { 587 * name: "Mr. Body" 588 * } 589 * recipients: [ 590 * Document { 591 * name: "John Doe" 592 * } 593 * Document { 594 * name: "Jane Doe" 595 * } 596 * ] 597 * subject: "IMPORTANT" 598 * } 599 * }</pre> 600 */ 601 @NonNull addProjection( @onNull String schema, @NonNull Collection<String> propertyPaths)602 public SearchSpec.Builder addProjection( 603 @NonNull String schema, @NonNull Collection<String> propertyPaths) { 604 Objects.requireNonNull(schema); 605 Objects.requireNonNull(propertyPaths); 606 resetIfBuilt(); 607 ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size()); 608 for (String propertyPath : propertyPaths) { 609 Objects.requireNonNull(propertyPath); 610 propertyPathsArrayList.add(propertyPath); 611 } 612 mProjectionTypePropertyMasks.putStringArrayList(schema, propertyPathsArrayList); 613 return this; 614 } 615 616 /** 617 * Set the maximum number of results to return for each group, where groups are defined by 618 * grouping type. 619 * 620 * <p>Calling this method will override any previous calls. So calling 621 * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 7) and then calling 622 * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 2) will result in only the latter, a limit 623 * of two results per package, being applied. Or calling setResultGrouping 624 * (GROUPING_TYPE_PER_PACKAGE, 1) and then calling setResultGrouping 625 * (GROUPING_TYPE_PER_PACKAGE | GROUPING_PER_NAMESPACE, 5) will result in five results per 626 * package per namespace. 627 * 628 * @param groupingTypeFlags One or more combination of grouping types. 629 * @param limit Number of results to return per {@code groupingTypeFlags}. 630 * @throws IllegalArgumentException if groupingTypeFlags is zero. 631 */ 632 // Individual parameters available from getResultGroupingTypeFlags and 633 // getResultGroupingLimit 634 @SuppressLint("MissingGetterMatchingBuilder") 635 @NonNull setResultGrouping(@roupingType int groupingTypeFlags, int limit)636 public Builder setResultGrouping(@GroupingType int groupingTypeFlags, int limit) { 637 Preconditions.checkState( 638 groupingTypeFlags != 0, "Result grouping type cannot be zero."); 639 resetIfBuilt(); 640 mGroupingTypeFlags = groupingTypeFlags; 641 mGroupingLimit = limit; 642 return this; 643 } 644 645 /** Constructs a new {@link SearchSpec} from the contents of this builder. */ 646 @NonNull build()647 public SearchSpec build() { 648 Bundle bundle = new Bundle(); 649 bundle.putStringArrayList(SCHEMA_FIELD, mSchemas); 650 bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces); 651 bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames); 652 bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks); 653 bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage); 654 bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType); 655 bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount); 656 bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty); 657 bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize); 658 bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy); 659 bundle.putInt(ORDER_FIELD, mOrder); 660 bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags); 661 bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit); 662 mBuilt = true; 663 return new SearchSpec(bundle); 664 } 665 resetIfBuilt()666 private void resetIfBuilt() { 667 if (mBuilt) { 668 mSchemas = new ArrayList<>(mSchemas); 669 mNamespaces = new ArrayList<>(mNamespaces); 670 mPackageNames = new ArrayList<>(mPackageNames); 671 mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks); 672 mBuilt = false; 673 } 674 } 675 } 676 } 677