1 /* 2 * Copyright 2022 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.FlaggedApi; 20 import android.annotation.IntDef; 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.SuppressLint; 25 import android.app.appsearch.annotation.CanIgnoreReturnValue; 26 import android.app.appsearch.safeparcel.AbstractSafeParcelable; 27 import android.app.appsearch.safeparcel.SafeParcelable; 28 import android.app.appsearch.util.BundleUtil; 29 import android.os.Bundle; 30 import android.os.Parcel; 31 import android.os.Parcelable; 32 import android.util.ArrayMap; 33 import android.util.ArraySet; 34 35 import com.android.appsearch.flags.Flags; 36 import com.android.internal.util.Preconditions; 37 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.Collection; 43 import java.util.Collections; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Objects; 47 import java.util.Set; 48 49 /** 50 * This class represents the specification logic for AppSearch. It can be used to set the filter and 51 * settings of search a suggestions. 52 * 53 * @see AppSearchSession#searchSuggestion 54 */ 55 @SafeParcelable.Class(creator = "SearchSuggestionSpecCreator") 56 // TODO(b/384721898): Switch to JSpecify annotations 57 @SuppressWarnings({"HiddenSuperclass", "JSpecifyNullness"}) 58 public final class SearchSuggestionSpec extends AbstractSafeParcelable { 59 60 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 61 public static final @NonNull Parcelable.Creator<SearchSuggestionSpec> CREATOR = 62 new SearchSuggestionSpecCreator(); 63 64 @Field(id = 1, getter = "getFilterNamespaces") 65 private final @NonNull List<String> mFilterNamespaces; 66 67 @Field(id = 2, getter = "getFilterSchemas") 68 private final @NonNull List<String> mFilterSchemas; 69 70 // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is 71 // schema type and value is a list of target property paths in that schema to search over. 72 @Field(id = 3) 73 final @NonNull Bundle mFilterProperties; 74 75 // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is 76 // namespace and value is a list of target document ids in that namespace to search over. 77 @Field(id = 4) 78 final @NonNull Bundle mFilterDocumentIds; 79 80 @Field(id = 5, getter = "getRankingStrategy") 81 private final int mRankingStrategy; 82 83 @Field(id = 6, getter = "getMaximumResultCount") 84 private final int mMaximumResultCount; 85 86 @Field(id = 7, getter = "getSearchStringParameters") 87 private final @NonNull List<String> mSearchStringParameters; 88 89 /** @hide */ 90 @Constructor SearchSuggestionSpec( @aramid = 1) @onNull List<String> filterNamespaces, @Param(id = 2) @NonNull List<String> filterSchemas, @Param(id = 3) @NonNull Bundle filterProperties, @Param(id = 4) @NonNull Bundle filterDocumentIds, @Param(id = 5) @SuggestionRankingStrategy int rankingStrategy, @Param(id = 6) int maximumResultCount, @Param(id = 7) @Nullable List<String> searchStringParameters)91 public SearchSuggestionSpec( 92 @Param(id = 1) @NonNull List<String> filterNamespaces, 93 @Param(id = 2) @NonNull List<String> filterSchemas, 94 @Param(id = 3) @NonNull Bundle filterProperties, 95 @Param(id = 4) @NonNull Bundle filterDocumentIds, 96 @Param(id = 5) @SuggestionRankingStrategy int rankingStrategy, 97 @Param(id = 6) int maximumResultCount, 98 @Param(id = 7) @Nullable List<String> searchStringParameters) { 99 Preconditions.checkArgument( 100 maximumResultCount >= 1, "MaximumResultCount must be positive."); 101 mFilterNamespaces = Objects.requireNonNull(filterNamespaces); 102 mFilterSchemas = Objects.requireNonNull(filterSchemas); 103 mFilterProperties = Objects.requireNonNull(filterProperties); 104 mFilterDocumentIds = Objects.requireNonNull(filterDocumentIds); 105 mRankingStrategy = rankingStrategy; 106 mMaximumResultCount = maximumResultCount; 107 mSearchStringParameters = 108 (searchStringParameters != null) 109 ? Collections.unmodifiableList(searchStringParameters) 110 : Collections.emptyList(); 111 } 112 113 /** 114 * Ranking Strategy for {@link SearchSuggestionResult}. 115 * 116 * @hide 117 */ 118 @IntDef( 119 value = { 120 SUGGESTION_RANKING_STRATEGY_NONE, 121 SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT, 122 SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY, 123 }) 124 @Retention(RetentionPolicy.SOURCE) 125 public @interface SuggestionRankingStrategy {} 126 127 /** 128 * Ranked by the document count that contains the term. 129 * 130 * <p>Suppose the following document is in the index. 131 * 132 * <pre>Doc1 contains: term1 term2 term2 term2</pre> 133 * 134 * <pre>Doc2 contains: term1</pre> 135 * 136 * <p>Then, suppose that a search suggestion for "t" is issued with the DOCUMENT_COUNT, the 137 * returned {@link SearchSuggestionResult}s will be: term1, term2. The term1 will have higher 138 * score and appear in the results first. 139 */ 140 public static final int SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT = 0; 141 142 /** 143 * Ranked by the term appear frequency. 144 * 145 * <p>Suppose the following document is in the index. 146 * 147 * <pre>Doc1 contains: term1 term2 term2 term2</pre> 148 * 149 * <pre>Doc2 contains: term1</pre> 150 * 151 * <p>Then, suppose that a search suggestion for "t" is issued with the TERM_FREQUENCY, the 152 * returned {@link SearchSuggestionResult}s will be: term2, term1. The term2 will have higher 153 * score and appear in the results first. 154 */ 155 public static final int SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY = 1; 156 157 /** No Ranking, results are returned in arbitrary order. */ 158 public static final int SUGGESTION_RANKING_STRATEGY_NONE = 2; 159 160 /** 161 * Returns the maximum number of wanted suggestion that will be returned in the result object. 162 */ getMaximumResultCount()163 public int getMaximumResultCount() { 164 return mMaximumResultCount; 165 } 166 167 /** 168 * Returns the list of namespaces to search over. 169 * 170 * <p>If empty, will search over all namespaces. 171 */ getFilterNamespaces()172 public @NonNull List<String> getFilterNamespaces() { 173 if (mFilterNamespaces == null) { 174 return Collections.emptyList(); 175 } 176 return Collections.unmodifiableList(mFilterNamespaces); 177 } 178 179 /** Returns the ranking strategy. */ 180 @SuggestionRankingStrategy getRankingStrategy()181 public int getRankingStrategy() { 182 return mRankingStrategy; 183 } 184 185 /** 186 * Returns the list of schema to search the suggestion over. 187 * 188 * <p>If empty, will search over all schemas. 189 */ getFilterSchemas()190 public @NonNull List<String> getFilterSchemas() { 191 if (mFilterSchemas == null) { 192 return Collections.emptyList(); 193 } 194 return Collections.unmodifiableList(mFilterSchemas); 195 } 196 197 /** 198 * Returns the map of schema and target properties to search over. 199 * 200 * <p>The keys of the returned map are schema types, and the values are the target property path 201 * in that schema to search over. 202 * 203 * <p>If {@link Builder#addFilterPropertyPaths} was never called, returns an empty map. In this 204 * case AppSearch will search over all schemas and properties. 205 * 206 * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this 207 * function, rather than calling it multiple times. 208 */ 209 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) getFilterProperties()210 public @NonNull Map<String, List<String>> getFilterProperties() { 211 Set<String> schemas = mFilterProperties.keySet(); 212 Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size()); 213 for (String schema : schemas) { 214 typePropertyPathsMap.put( 215 schema, Objects.requireNonNull(mFilterProperties.getStringArrayList(schema))); 216 } 217 return typePropertyPathsMap; 218 } 219 220 /** 221 * Returns the map of namespace and target document ids to search over. 222 * 223 * <p>The keys of the returned map are namespaces, and the values are the target document ids in 224 * that namespace to search over. 225 * 226 * <p>If {@link Builder#addFilterDocumentIds} was never called, returns an empty map. In this 227 * case AppSearch will search over all namespace and document ids. 228 * 229 * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this 230 * function, rather than calling it multiple times. 231 */ getFilterDocumentIds()232 public @NonNull Map<String, List<String>> getFilterDocumentIds() { 233 Set<String> namespaces = mFilterDocumentIds.keySet(); 234 Map<String, List<String>> documentIdsMap = new ArrayMap<>(namespaces.size()); 235 for (String namespace : namespaces) { 236 documentIdsMap.put( 237 namespace, 238 Objects.requireNonNull(mFilterDocumentIds.getStringArrayList(namespace))); 239 } 240 return documentIdsMap; 241 } 242 243 /** 244 * Returns the list of String parameters that can be referenced in the query through the 245 * "getSearchStringParameter({index})" function. 246 * 247 * @see AppSearchSession#search 248 */ 249 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS) getSearchStringParameters()250 public @NonNull List<String> getSearchStringParameters() { 251 return mSearchStringParameters; 252 } 253 254 /** Builder for {@link SearchSuggestionSpec objects}. */ 255 public static final class Builder { 256 private ArrayList<String> mNamespaces = new ArrayList<>(); 257 private ArrayList<String> mSchemas = new ArrayList<>(); 258 private Bundle mTypePropertyFilters = new Bundle(); 259 private Bundle mDocumentIds = new Bundle(); 260 private final int mTotalResultCount; 261 262 @SuggestionRankingStrategy 263 private int mRankingStrategy = SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT; 264 265 private List<String> mSearchStringParameters = new ArrayList<>(); 266 private boolean mBuilt = false; 267 268 /** 269 * Creates an {@link SearchSuggestionSpec.Builder} object. 270 * 271 * @param maximumResultCount Sets the maximum number of suggestion in the returned object. 272 */ Builder(@ntRangefrom = 1) int maximumResultCount)273 public Builder(@IntRange(from = 1) int maximumResultCount) { 274 Preconditions.checkArgument( 275 maximumResultCount >= 1, "maximumResultCount must be positive."); 276 mTotalResultCount = maximumResultCount; 277 } 278 279 /** 280 * Adds a namespace filter to {@link SearchSuggestionSpec} Entry. Only search for 281 * suggestions that has documents under the specified namespaces. 282 * 283 * <p>If unset, the query will search over all namespaces. 284 */ 285 @CanIgnoreReturnValue addFilterNamespaces(@onNull String... namespaces)286 public @NonNull Builder addFilterNamespaces(@NonNull String... namespaces) { 287 Objects.requireNonNull(namespaces); 288 resetIfBuilt(); 289 return addFilterNamespaces(Arrays.asList(namespaces)); 290 } 291 292 /** 293 * Adds a namespace filter to {@link SearchSuggestionSpec} Entry. Only search for 294 * suggestions that has documents under the specified namespaces. 295 * 296 * <p>If unset, the query will search over all namespaces. 297 */ 298 @CanIgnoreReturnValue addFilterNamespaces(@onNull Collection<String> namespaces)299 public @NonNull Builder addFilterNamespaces(@NonNull Collection<String> namespaces) { 300 Objects.requireNonNull(namespaces); 301 resetIfBuilt(); 302 mNamespaces.addAll(namespaces); 303 return this; 304 } 305 306 /** 307 * Sets ranking strategy for suggestion results. 308 * 309 * <p>The default value {@link #SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT} will be used if 310 * this method is never called. 311 */ 312 @CanIgnoreReturnValue setRankingStrategy(@uggestionRankingStrategy int rankingStrategy)313 public @NonNull Builder setRankingStrategy(@SuggestionRankingStrategy int rankingStrategy) { 314 Preconditions.checkArgumentInRange( 315 rankingStrategy, 316 SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT, 317 SUGGESTION_RANKING_STRATEGY_NONE, 318 "Suggestion ranking strategy"); 319 resetIfBuilt(); 320 mRankingStrategy = rankingStrategy; 321 return this; 322 } 323 324 /** 325 * Adds a schema filter to {@link SearchSuggestionSpec} Entry. Only search for suggestions 326 * that has documents under the specified schema. 327 * 328 * <p>If unset, the query will search over all schema. 329 */ 330 @CanIgnoreReturnValue addFilterSchemas(@onNull String... schemaTypes)331 public @NonNull Builder addFilterSchemas(@NonNull String... schemaTypes) { 332 Objects.requireNonNull(schemaTypes); 333 resetIfBuilt(); 334 return addFilterSchemas(Arrays.asList(schemaTypes)); 335 } 336 337 /** 338 * Adds a schema filter to {@link SearchSuggestionSpec} Entry. Only search for suggestions 339 * that has documents under the specified schema. 340 * 341 * <p>If unset, the query will search over all schema. 342 */ 343 @CanIgnoreReturnValue addFilterSchemas(@onNull Collection<String> schemaTypes)344 public @NonNull Builder addFilterSchemas(@NonNull Collection<String> schemaTypes) { 345 Objects.requireNonNull(schemaTypes); 346 resetIfBuilt(); 347 mSchemas.addAll(schemaTypes); 348 return this; 349 } 350 351 /** 352 * Adds property paths for the specified type to the property filter of {@link 353 * SearchSuggestionSpec} Entry. Only search for suggestions that has content under the 354 * specified property. If property paths are added for a type, then only the properties 355 * referred to will be retrieved for results of that type. 356 * 357 * <p>If a property path that is specified isn't present in a result, it will be ignored for 358 * that result. Property paths cannot be null. 359 * 360 * <p>If no property paths are added for a particular type, then all properties of results 361 * of that type will be retrieved. 362 * 363 * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. 364 * 365 * @param schema the {@link AppSearchSchema} that contains the target properties 366 * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited sequence 367 * of property names indicating which property in the document these snippets correspond 368 * to. 369 */ 370 @CanIgnoreReturnValue 371 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) addFilterProperties( @onNull String schema, @NonNull Collection<String> propertyPaths)372 public @NonNull Builder addFilterProperties( 373 @NonNull String schema, @NonNull Collection<String> propertyPaths) { 374 Objects.requireNonNull(schema); 375 Objects.requireNonNull(propertyPaths); 376 resetIfBuilt(); 377 ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size()); 378 for (String propertyPath : propertyPaths) { 379 Objects.requireNonNull(propertyPath); 380 propertyPathsArrayList.add(propertyPath); 381 } 382 mTypePropertyFilters.putStringArrayList(schema, propertyPathsArrayList); 383 return this; 384 } 385 386 /** 387 * Adds property paths for the specified type to the property filter of {@link 388 * SearchSuggestionSpec} Entry. Only search for suggestions that has content under the 389 * specified property. If property paths are added for a type, then only the properties 390 * referred to will be retrieved for results of that type. 391 * 392 * <p>If a property path that is specified isn't present in a result, it will be ignored for 393 * that result. Property paths cannot be null. 394 * 395 * <p>If no property paths are added for a particular type, then all properties of results 396 * of that type will be retrieved. 397 * 398 * @param schema the {@link AppSearchSchema} that contains the target properties 399 * @param propertyPaths The {@link PropertyPath} to search suggestion over 400 */ 401 @CanIgnoreReturnValue 402 // Getter method is getFilterProperties 403 @SuppressLint("MissingGetterMatchingBuilder") 404 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) addFilterPropertyPaths( @onNull String schema, @NonNull Collection<PropertyPath> propertyPaths)405 public @NonNull Builder addFilterPropertyPaths( 406 @NonNull String schema, @NonNull Collection<PropertyPath> propertyPaths) { 407 Objects.requireNonNull(schema); 408 Objects.requireNonNull(propertyPaths); 409 ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size()); 410 for (PropertyPath propertyPath : propertyPaths) { 411 propertyPathsArrayList.add(propertyPath.toString()); 412 } 413 return addFilterProperties(schema, propertyPathsArrayList); 414 } 415 416 /** 417 * Adds a document ID filter to {@link SearchSuggestionSpec} Entry. Only search for 418 * suggestions in the given specified documents. 419 * 420 * <p>If unset, the query will search over all documents. 421 */ 422 @CanIgnoreReturnValue addFilterDocumentIds( @onNull String namespace, @NonNull String... documentIds)423 public @NonNull Builder addFilterDocumentIds( 424 @NonNull String namespace, @NonNull String... documentIds) { 425 Objects.requireNonNull(namespace); 426 Objects.requireNonNull(documentIds); 427 resetIfBuilt(); 428 return addFilterDocumentIds(namespace, Arrays.asList(documentIds)); 429 } 430 431 /** 432 * Adds a document ID filter to {@link SearchSuggestionSpec} Entry. Only search for 433 * suggestions in the given specified documents. 434 * 435 * <p>If unset, the query will search over all documents. 436 */ 437 @CanIgnoreReturnValue addFilterDocumentIds( @onNull String namespace, @NonNull Collection<String> documentIds)438 public @NonNull Builder addFilterDocumentIds( 439 @NonNull String namespace, @NonNull Collection<String> documentIds) { 440 Objects.requireNonNull(namespace); 441 Objects.requireNonNull(documentIds); 442 resetIfBuilt(); 443 ArrayList<String> documentIdList = new ArrayList<>(documentIds.size()); 444 for (String documentId : documentIds) { 445 documentIdList.add(Objects.requireNonNull(documentId)); 446 } 447 mDocumentIds.putStringArrayList(namespace, documentIdList); 448 return this; 449 } 450 451 /** 452 * Adds Strings to the list of String parameters that can be referenced in the query through 453 * the "getSearchStringParameter({index})" function. 454 * 455 * @see AppSearchSession#search 456 */ 457 @CanIgnoreReturnValue 458 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS) addSearchStringParameters( @onNull String... searchStringParameters)459 public @NonNull Builder addSearchStringParameters( 460 @NonNull String... searchStringParameters) { 461 Objects.requireNonNull(searchStringParameters); 462 resetIfBuilt(); 463 return addSearchStringParameters(Arrays.asList(searchStringParameters)); 464 } 465 466 /** 467 * Adds Strings to the list of String parameters that can be referenced in the query through 468 * the "getSearchStringParameter({index})" function. 469 * 470 * @see AppSearchSession#search 471 */ 472 @CanIgnoreReturnValue 473 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SEARCH_STRING_PARAMETERS) addSearchStringParameters( @onNull List<String> searchStringParameters)474 public @NonNull Builder addSearchStringParameters( 475 @NonNull List<String> searchStringParameters) { 476 Objects.requireNonNull(searchStringParameters); 477 resetIfBuilt(); 478 mSearchStringParameters.addAll(searchStringParameters); 479 return this; 480 } 481 482 /** Constructs a new {@link SearchSpec} from the contents of this builder. */ build()483 public @NonNull SearchSuggestionSpec build() { 484 if (!mSchemas.isEmpty()) { 485 Set<String> schemaFilter = new ArraySet<>(mSchemas); 486 for (String schema : mTypePropertyFilters.keySet()) { 487 if (!schemaFilter.contains(schema)) { 488 throw new IllegalStateException( 489 "The schema: " 490 + schema 491 + " exists in the property filter but " 492 + "doesn't exist in the schema filter."); 493 } 494 } 495 } 496 if (!mNamespaces.isEmpty()) { 497 Set<String> namespaceFilter = new ArraySet<>(mNamespaces); 498 for (String namespace : mDocumentIds.keySet()) { 499 if (!namespaceFilter.contains(namespace)) { 500 throw new IllegalStateException( 501 "The namespace: " 502 + namespace 503 + " exists in the document id " 504 + "filter but doesn't exist in the namespace filter."); 505 } 506 } 507 } 508 mBuilt = true; 509 return new SearchSuggestionSpec( 510 mNamespaces, 511 mSchemas, 512 mTypePropertyFilters, 513 mDocumentIds, 514 mRankingStrategy, 515 mTotalResultCount, 516 mSearchStringParameters); 517 } 518 resetIfBuilt()519 private void resetIfBuilt() { 520 if (mBuilt) { 521 mNamespaces = new ArrayList<>(mNamespaces); 522 mSchemas = new ArrayList<>(mSchemas); 523 mTypePropertyFilters = BundleUtil.deepCopy(mTypePropertyFilters); 524 mDocumentIds = BundleUtil.deepCopy(mDocumentIds); 525 mSearchStringParameters = new ArrayList<>(mSearchStringParameters); 526 mBuilt = false; 527 } 528 } 529 } 530 531 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 532 @Override writeToParcel(@onNull Parcel dest, int flags)533 public void writeToParcel(@NonNull Parcel dest, int flags) { 534 SearchSuggestionSpecCreator.writeToParcel(this, dest, flags); 535 } 536 } 537