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.NonNull; 20 import android.annotation.Nullable; 21 import android.app.appsearch.annotation.CanIgnoreReturnValue; 22 import android.os.Bundle; 23 24 import com.android.internal.util.Preconditions; 25 26 import java.util.ArrayList; 27 import java.util.List; 28 import java.util.Objects; 29 30 /** 31 * This class represents one of the results obtained from an AppSearch query. 32 * 33 * <p>This allows clients to obtain: 34 * 35 * <ul> 36 * <li>The document which matched, using {@link #getGenericDocument} 37 * <li>Information about which properties in the document matched, and "snippet" information 38 * containing textual summaries of the document's matches, using {@link #getMatchInfos} 39 * </ul> 40 * 41 * <p>"Snippet" refers to a substring of text from the content of document that is returned as a 42 * part of search result. 43 * 44 * @see SearchResults 45 */ 46 public final class SearchResult { 47 static final String DOCUMENT_FIELD = "document"; 48 static final String MATCH_INFOS_FIELD = "matchInfos"; 49 static final String PACKAGE_NAME_FIELD = "packageName"; 50 static final String DATABASE_NAME_FIELD = "databaseName"; 51 static final String RANKING_SIGNAL_FIELD = "rankingSignal"; 52 static final String JOINED_RESULTS = "joinedResults"; 53 54 @NonNull private final Bundle mBundle; 55 56 /** Cache of the inflated document. Comes from inflating mDocumentBundle at first use. */ 57 @Nullable private GenericDocument mDocument; 58 59 /** Cache of the inflated matches. Comes from inflating mMatchBundles at first use. */ 60 @Nullable private List<MatchInfo> mMatchInfos; 61 62 /** @hide */ SearchResult(@onNull Bundle bundle)63 public SearchResult(@NonNull Bundle bundle) { 64 mBundle = Objects.requireNonNull(bundle); 65 } 66 67 /** @hide */ 68 @NonNull getBundle()69 public Bundle getBundle() { 70 return mBundle; 71 } 72 73 /** 74 * Contains the matching {@link GenericDocument}. 75 * 76 * @return Document object which matched the query. 77 */ 78 @NonNull getGenericDocument()79 public GenericDocument getGenericDocument() { 80 if (mDocument == null) { 81 mDocument = 82 new GenericDocument(Objects.requireNonNull(mBundle.getBundle(DOCUMENT_FIELD))); 83 } 84 return mDocument; 85 } 86 87 /** 88 * Returns a list of {@link MatchInfo}s providing information about how the document in {@link 89 * #getGenericDocument} matched the query. 90 * 91 * @return List of matches based on {@link SearchSpec}. If snippeting is disabled using {@link 92 * SearchSpec.Builder#setSnippetCount} or {@link 93 * SearchSpec.Builder#setSnippetCountPerProperty}, for all results after that value, this 94 * method returns an empty list. 95 */ 96 @NonNull 97 @SuppressWarnings("deprecation") getMatchInfos()98 public List<MatchInfo> getMatchInfos() { 99 if (mMatchInfos == null) { 100 List<Bundle> matchBundles = 101 Objects.requireNonNull(mBundle.getParcelableArrayList(MATCH_INFOS_FIELD)); 102 mMatchInfos = new ArrayList<>(matchBundles.size()); 103 for (int i = 0; i < matchBundles.size(); i++) { 104 MatchInfo matchInfo = new MatchInfo(matchBundles.get(i), getGenericDocument()); 105 if (mMatchInfos != null) { 106 // This additional check is added for NullnessChecker. 107 mMatchInfos.add(matchInfo); 108 } 109 } 110 } 111 // This check is added for NullnessChecker, mMatchInfos will always be NonNull. 112 return Objects.requireNonNull(mMatchInfos); 113 } 114 115 /** 116 * Contains the package name of the app that stored the {@link GenericDocument}. 117 * 118 * @return Package name that stored the document 119 */ 120 @NonNull getPackageName()121 public String getPackageName() { 122 return Objects.requireNonNull(mBundle.getString(PACKAGE_NAME_FIELD)); 123 } 124 125 /** 126 * Contains the database name that stored the {@link GenericDocument}. 127 * 128 * @return Name of the database within which the document is stored 129 */ 130 @NonNull getDatabaseName()131 public String getDatabaseName() { 132 return Objects.requireNonNull(mBundle.getString(DATABASE_NAME_FIELD)); 133 } 134 135 /** 136 * Returns the ranking signal of the {@link GenericDocument}, according to the ranking strategy 137 * set in {@link SearchSpec.Builder#setRankingStrategy(int)}. 138 * 139 * <p>The meaning of the ranking signal and its value is determined by the selected ranking 140 * strategy: 141 * 142 * <ul> 143 * <li>{@link SearchSpec#RANKING_STRATEGY_NONE} - this value will be 0 144 * <li>{@link SearchSpec#RANKING_STRATEGY_DOCUMENT_SCORE} - the value returned by calling 145 * {@link GenericDocument#getScore()} on the document returned by {@link 146 * #getGenericDocument()} 147 * <li>{@link SearchSpec#RANKING_STRATEGY_CREATION_TIMESTAMP} - the value returned by calling 148 * {@link GenericDocument#getCreationTimestampMillis()} on the document returned by {@link 149 * #getGenericDocument()} 150 * <li>{@link SearchSpec#RANKING_STRATEGY_RELEVANCE_SCORE} - an arbitrary double value where a 151 * higher value means more relevant 152 * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} - the number of times usage has been 153 * reported for the document returned by {@link #getGenericDocument()} 154 * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} - the timestamp of the 155 * most recent usage that has been reported for the document returned by {@link 156 * #getGenericDocument()} 157 * </ul> 158 * 159 * @return Ranking signal of the document 160 */ getRankingSignal()161 public double getRankingSignal() { 162 return mBundle.getDouble(RANKING_SIGNAL_FIELD); 163 } 164 165 /** 166 * Gets a list of {@link SearchResult} joined from the join operation. 167 * 168 * <p>These joined documents match the outer document as specified in the {@link JoinSpec} with 169 * parentPropertyExpression and childPropertyExpression. They are ordered according to the 170 * {@link JoinSpec#getNestedSearchSpec}, and as many SearchResults as specified by {@link 171 * JoinSpec#getMaxJoinedResultCount} will be returned. If no {@link JoinSpec} was specified, 172 * this returns an empty list. 173 * 174 * <p>This method is inefficient to call repeatedly, as new {@link SearchResult} objects are 175 * created each time. 176 * 177 * @return a List of SearchResults containing joined documents. 178 */ 179 @NonNull 180 @SuppressWarnings("deprecation") // Bundle#getParcelableArrayList(String) is deprecated. getJoinedResults()181 public List<SearchResult> getJoinedResults() { 182 ArrayList<Bundle> bundles = mBundle.getParcelableArrayList(JOINED_RESULTS); 183 if (bundles == null) { 184 return new ArrayList<>(); 185 } 186 List<SearchResult> res = new ArrayList<>(bundles.size()); 187 for (int i = 0; i < bundles.size(); i++) { 188 res.add(new SearchResult(bundles.get(i))); 189 } 190 191 return res; 192 } 193 194 /** Builder for {@link SearchResult} objects. */ 195 public static final class Builder { 196 private final String mPackageName; 197 private final String mDatabaseName; 198 private ArrayList<Bundle> mMatchInfoBundles = new ArrayList<>(); 199 private GenericDocument mGenericDocument; 200 private double mRankingSignal; 201 private ArrayList<Bundle> mJoinedResults = new ArrayList<>(); 202 private boolean mBuilt = false; 203 204 /** 205 * Constructs a new builder for {@link SearchResult} objects. 206 * 207 * @param packageName the package name the matched document belongs to 208 * @param databaseName the database name the matched document belongs to. 209 */ Builder(@onNull String packageName, @NonNull String databaseName)210 public Builder(@NonNull String packageName, @NonNull String databaseName) { 211 mPackageName = Objects.requireNonNull(packageName); 212 mDatabaseName = Objects.requireNonNull(databaseName); 213 } 214 215 /** Sets the document which matched. */ 216 @CanIgnoreReturnValue 217 @NonNull setGenericDocument(@onNull GenericDocument document)218 public Builder setGenericDocument(@NonNull GenericDocument document) { 219 Objects.requireNonNull(document); 220 resetIfBuilt(); 221 mGenericDocument = document; 222 return this; 223 } 224 225 /** Adds another match to this SearchResult. */ 226 @CanIgnoreReturnValue 227 @NonNull addMatchInfo(@onNull MatchInfo matchInfo)228 public Builder addMatchInfo(@NonNull MatchInfo matchInfo) { 229 Preconditions.checkState( 230 matchInfo.mDocument == null, 231 "This MatchInfo is already associated with a SearchResult and can't be " 232 + "reassigned"); 233 resetIfBuilt(); 234 mMatchInfoBundles.add(matchInfo.mBundle); 235 return this; 236 } 237 238 /** Sets the ranking signal of the matched document in this SearchResult. */ 239 @CanIgnoreReturnValue 240 @NonNull setRankingSignal(double rankingSignal)241 public Builder setRankingSignal(double rankingSignal) { 242 resetIfBuilt(); 243 mRankingSignal = rankingSignal; 244 return this; 245 } 246 247 /** 248 * Adds a {@link SearchResult} that was joined by the {@link JoinSpec}. 249 * 250 * @param joinedResult The joined SearchResult to add. 251 */ 252 @CanIgnoreReturnValue 253 @NonNull addJoinedResult(@onNull SearchResult joinedResult)254 public Builder addJoinedResult(@NonNull SearchResult joinedResult) { 255 resetIfBuilt(); 256 mJoinedResults.add(joinedResult.getBundle()); 257 return this; 258 } 259 260 /** Constructs a new {@link SearchResult}. */ 261 @NonNull build()262 public SearchResult build() { 263 Bundle bundle = new Bundle(); 264 bundle.putString(PACKAGE_NAME_FIELD, mPackageName); 265 bundle.putString(DATABASE_NAME_FIELD, mDatabaseName); 266 bundle.putBundle(DOCUMENT_FIELD, mGenericDocument.getBundle()); 267 bundle.putDouble(RANKING_SIGNAL_FIELD, mRankingSignal); 268 bundle.putParcelableArrayList(MATCH_INFOS_FIELD, mMatchInfoBundles); 269 bundle.putParcelableArrayList(JOINED_RESULTS, mJoinedResults); 270 mBuilt = true; 271 return new SearchResult(bundle); 272 } 273 resetIfBuilt()274 private void resetIfBuilt() { 275 if (mBuilt) { 276 mMatchInfoBundles = new ArrayList<>(mMatchInfoBundles); 277 mJoinedResults = new ArrayList<>(mJoinedResults); 278 mBuilt = false; 279 } 280 } 281 } 282 283 /** 284 * This class represents match objects for any snippets that might be present in {@link 285 * SearchResults} from a query. Using this class, you can get: 286 * 287 * <ul> 288 * <li>the full text - all of the text in that String property 289 * <li>the exact term match - the 'term' (full word) that matched the query 290 * <li>the subterm match - the portion of the matched term that appears in the query 291 * <li>a suggested text snippet - a portion of the full text surrounding the exact term match, 292 * set to term boundaries. The size of the snippet is specified in {@link 293 * SearchSpec.Builder#setMaxSnippetSize} 294 * </ul> 295 * 296 * for each match in the document. 297 * 298 * <p>Class Example 1: 299 * 300 * <p>A document contains the following text in property "subject": 301 * 302 * <p>"A commonly used fake word is foo. Another nonsense word that’s used a lot is bar." 303 * 304 * <p>If the queryExpression is "foo" and {@link SearchSpec#getMaxSnippetSize} is 10, 305 * 306 * <ul> 307 * <li>{@link MatchInfo#getPropertyPath()} returns "subject" 308 * <li>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another 309 * nonsense word that’s used a lot is bar." 310 * <li>{@link MatchInfo#getExactMatchRange()} returns [29, 32] 311 * <li>{@link MatchInfo#getExactMatch()} returns "foo" 312 * <li>{@link MatchInfo#getSubmatchRange()} returns [29, 32] 313 * <li>{@link MatchInfo#getSubmatch()} returns "foo" 314 * <li>{@link MatchInfo#getSnippetRange()} returns [26, 33] 315 * <li>{@link MatchInfo#getSnippet()} returns "is foo." 316 * </ul> 317 * 318 * <p> 319 * 320 * <p>Class Example 2: 321 * 322 * <p>A document contains one property named "subject" and one property named "sender" which 323 * contains a "name" property. 324 * 325 * <p>In this case, we will have 2 property paths: {@code sender.name} and {@code subject}. 326 * 327 * <p>Let {@code sender.name = "Test Name Jr."} and {@code subject = "Testing 1 2 3"} 328 * 329 * <p>If the queryExpression is "Test" with {@link SearchSpec#TERM_MATCH_PREFIX} and {@link 330 * SearchSpec#getMaxSnippetSize} is 10. We will have 2 matches: 331 * 332 * <p>Match-1 333 * 334 * <ul> 335 * <li>{@link MatchInfo#getPropertyPath()} returns "sender.name" 336 * <li>{@link MatchInfo#getFullText()} returns "Test Name Jr." 337 * <li>{@link MatchInfo#getExactMatchRange()} returns [0, 4] 338 * <li>{@link MatchInfo#getExactMatch()} returns "Test" 339 * <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4] 340 * <li>{@link MatchInfo#getSubmatch()} returns "Test" 341 * <li>{@link MatchInfo#getSnippetRange()} returns [0, 9] 342 * <li>{@link MatchInfo#getSnippet()} returns "Test Name" 343 * </ul> 344 * 345 * <p>Match-2 346 * 347 * <ul> 348 * <li>{@link MatchInfo#getPropertyPath()} returns "subject" 349 * <li>{@link MatchInfo#getFullText()} returns "Testing 1 2 3" 350 * <li>{@link MatchInfo#getExactMatchRange()} returns [0, 7] 351 * <li>{@link MatchInfo#getExactMatch()} returns "Testing" 352 * <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4] 353 * <li>{@link MatchInfo#getSubmatch()} returns "Test" 354 * <li>{@link MatchInfo#getSnippetRange()} returns [0, 9] 355 * <li>{@link MatchInfo#getSnippet()} returns "Testing 1" 356 * </ul> 357 */ 358 public static final class MatchInfo { 359 /** The path of the matching snippet property. */ 360 private static final String PROPERTY_PATH_FIELD = "propertyPath"; 361 362 private static final String EXACT_MATCH_RANGE_LOWER_FIELD = "exactMatchRangeLower"; 363 private static final String EXACT_MATCH_RANGE_UPPER_FIELD = "exactMatchRangeUpper"; 364 private static final String SUBMATCH_RANGE_LOWER_FIELD = "submatchRangeLower"; 365 private static final String SUBMATCH_RANGE_UPPER_FIELD = "submatchRangeUpper"; 366 private static final String SNIPPET_RANGE_LOWER_FIELD = "snippetRangeLower"; 367 private static final String SNIPPET_RANGE_UPPER_FIELD = "snippetRangeUpper"; 368 369 private final String mPropertyPath; 370 @Nullable private PropertyPath mPropertyPathObject = null; 371 final Bundle mBundle; 372 373 /** 374 * Document which the match comes from. 375 * 376 * <p>If this is {@code null}, methods which require access to the document, like {@link 377 * #getExactMatch}, will throw {@link NullPointerException}. 378 */ 379 @Nullable final GenericDocument mDocument; 380 381 /** Full text of the matched property. Populated on first use. */ 382 @Nullable private String mFullText; 383 384 /** Range of property that exactly matched the query. Populated on first use. */ 385 @Nullable private MatchRange mExactMatchRange; 386 387 /** 388 * Range of property that corresponds to the subsequence of the exact match that directly 389 * matches a query term. Populated on first use. 390 */ 391 @Nullable private MatchRange mSubmatchRange; 392 393 /** Range of some reasonable amount of context around the query. Populated on first use. */ 394 @Nullable private MatchRange mWindowRange; 395 MatchInfo(@onNull Bundle bundle, @Nullable GenericDocument document)396 MatchInfo(@NonNull Bundle bundle, @Nullable GenericDocument document) { 397 mBundle = Objects.requireNonNull(bundle); 398 mDocument = document; 399 mPropertyPath = Objects.requireNonNull(bundle.getString(PROPERTY_PATH_FIELD)); 400 } 401 402 /** 403 * Gets the property path corresponding to the given entry. 404 * 405 * <p>A property path is a '.' - delimited sequence of property names indicating which 406 * property in the document these snippets correspond to. 407 * 408 * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. For class 409 * example 1 this returns "subject" 410 */ 411 @NonNull getPropertyPath()412 public String getPropertyPath() { 413 return mPropertyPath; 414 } 415 416 /** 417 * Gets a {@link PropertyPath} object representing the property path corresponding to the 418 * given entry. 419 * 420 * <p>Methods such as {@link GenericDocument#getPropertyDocument} accept a path as a string 421 * rather than a {@link PropertyPath} object. However, you may want to manipulate the path 422 * before getting a property document. This method returns a {@link PropertyPath} rather 423 * than a String for easier path manipulation, which can then be converted to a String. 424 * 425 * @see #getPropertyPath 426 * @see PropertyPath 427 */ 428 @NonNull getPropertyPathObject()429 public PropertyPath getPropertyPathObject() { 430 if (mPropertyPathObject == null) { 431 mPropertyPathObject = new PropertyPath(mPropertyPath); 432 } 433 return mPropertyPathObject; 434 } 435 436 /** 437 * Gets the full text corresponding to the given entry. 438 * 439 * <p>Class example 1: this returns "A commonly used fake word is foo. Another nonsense word 440 * that's used a lot is bar." 441 * 442 * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name Jr." and, 443 * for the second {@link MatchInfo}, this returns "Testing 1 2 3". 444 */ 445 @NonNull getFullText()446 public String getFullText() { 447 if (mFullText == null) { 448 if (mDocument == null) { 449 throw new IllegalStateException( 450 "Document has not been populated; this MatchInfo cannot be used yet"); 451 } 452 mFullText = getPropertyValues(mDocument, mPropertyPath); 453 } 454 return mFullText; 455 } 456 457 /** 458 * Gets the {@link MatchRange} of the exact term of the given entry that matched the query. 459 * 460 * <p>Class example 1: this returns [29, 32]. 461 * 462 * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 4] and, for the 463 * second {@link MatchInfo}, this returns [0, 7]. 464 */ 465 @NonNull getExactMatchRange()466 public MatchRange getExactMatchRange() { 467 if (mExactMatchRange == null) { 468 mExactMatchRange = 469 new MatchRange( 470 mBundle.getInt(EXACT_MATCH_RANGE_LOWER_FIELD), 471 mBundle.getInt(EXACT_MATCH_RANGE_UPPER_FIELD)); 472 } 473 return mExactMatchRange; 474 } 475 476 /** 477 * Gets the exact term of the given entry that matched the query. 478 * 479 * <p>Class example 1: this returns "foo". 480 * 481 * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test" and, for the 482 * second {@link MatchInfo}, this returns "Testing". 483 */ 484 @NonNull getExactMatch()485 public CharSequence getExactMatch() { 486 return getSubstring(getExactMatchRange()); 487 } 488 489 /** 490 * Gets the {@link MatchRange} of the exact term subsequence of the given entry that matched 491 * the query. 492 * 493 * <p>Class example 1: this returns [29, 32]. 494 * 495 * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 4] and, for the 496 * second {@link MatchInfo}, this returns [0, 4]. 497 */ 498 @NonNull getSubmatchRange()499 public MatchRange getSubmatchRange() { 500 checkSubmatchSupported(); 501 if (mSubmatchRange == null) { 502 mSubmatchRange = 503 new MatchRange( 504 mBundle.getInt(SUBMATCH_RANGE_LOWER_FIELD), 505 mBundle.getInt(SUBMATCH_RANGE_UPPER_FIELD)); 506 } 507 return mSubmatchRange; 508 } 509 510 /** 511 * Gets the exact term subsequence of the given entry that matched the query. 512 * 513 * <p>Class example 1: this returns "foo". 514 * 515 * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test" and, for the 516 * second {@link MatchInfo}, this returns "Test". 517 */ 518 @NonNull getSubmatch()519 public CharSequence getSubmatch() { 520 checkSubmatchSupported(); 521 return getSubstring(getSubmatchRange()); 522 } 523 524 /** 525 * Gets the snippet {@link MatchRange} corresponding to the given entry. 526 * 527 * <p>Only populated when set maxSnippetSize > 0 in {@link 528 * SearchSpec.Builder#setMaxSnippetSize}. 529 * 530 * <p>Class example 1: this returns [29, 41]. 531 * 532 * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 9] and, for the 533 * second {@link MatchInfo}, this returns [0, 13]. 534 */ 535 @NonNull getSnippetRange()536 public MatchRange getSnippetRange() { 537 if (mWindowRange == null) { 538 mWindowRange = 539 new MatchRange( 540 mBundle.getInt(SNIPPET_RANGE_LOWER_FIELD), 541 mBundle.getInt(SNIPPET_RANGE_UPPER_FIELD)); 542 } 543 return mWindowRange; 544 } 545 546 /** 547 * Gets the snippet corresponding to the given entry. 548 * 549 * <p>Snippet - Provides a subset of the content to display. Only populated when requested 550 * maxSnippetSize > 0. The size of this content can be changed by {@link 551 * SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of the 552 * matched token with content on either side clipped to token boundaries. 553 * 554 * <p>Class example 1: this returns "foo. Another". 555 * 556 * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name" and, for 557 * the second {@link MatchInfo}, this returns "Testing 1 2 3". 558 */ 559 @NonNull getSnippet()560 public CharSequence getSnippet() { 561 return getSubstring(getSnippetRange()); 562 } 563 getSubstring(MatchRange range)564 private CharSequence getSubstring(MatchRange range) { 565 return getFullText().substring(range.getStart(), range.getEnd()); 566 } 567 checkSubmatchSupported()568 private void checkSubmatchSupported() { 569 if (!mBundle.containsKey(SUBMATCH_RANGE_LOWER_FIELD)) { 570 throw new UnsupportedOperationException( 571 "Submatch is not supported with this backend/Android API level " 572 + "combination"); 573 } 574 } 575 576 /** Extracts the matching string from the document. */ getPropertyValues(GenericDocument document, String propertyName)577 private static String getPropertyValues(GenericDocument document, String propertyName) { 578 String result = document.getPropertyString(propertyName); 579 if (result == null) { 580 throw new IllegalStateException( 581 "No content found for requested property path: " + propertyName); 582 } 583 return result; 584 } 585 586 /** Builder for {@link MatchInfo} objects. */ 587 public static final class Builder { 588 private final String mPropertyPath; 589 private MatchRange mExactMatchRange = new MatchRange(0, 0); 590 @Nullable private MatchRange mSubmatchRange; 591 private MatchRange mSnippetRange = new MatchRange(0, 0); 592 593 /** 594 * Creates a new {@link MatchInfo.Builder} reporting a match with the given property 595 * path. 596 * 597 * <p>A property path is a dot-delimited sequence of property names indicating which 598 * property in the document these snippets correspond to. 599 * 600 * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. For class 601 * example 1, this returns "subject". 602 * 603 * @param propertyPath A dot-delimited sequence of property names indicating which 604 * property in the document these snippets correspond to. 605 */ Builder(@onNull String propertyPath)606 public Builder(@NonNull String propertyPath) { 607 mPropertyPath = Objects.requireNonNull(propertyPath); 608 } 609 610 /** Sets the exact {@link MatchRange} corresponding to the given entry. */ 611 @CanIgnoreReturnValue 612 @NonNull setExactMatchRange(@onNull MatchRange matchRange)613 public Builder setExactMatchRange(@NonNull MatchRange matchRange) { 614 mExactMatchRange = Objects.requireNonNull(matchRange); 615 return this; 616 } 617 618 /** Sets the submatch {@link MatchRange} corresponding to the given entry. */ 619 @CanIgnoreReturnValue 620 @NonNull setSubmatchRange(@onNull MatchRange matchRange)621 public Builder setSubmatchRange(@NonNull MatchRange matchRange) { 622 mSubmatchRange = Objects.requireNonNull(matchRange); 623 return this; 624 } 625 626 /** Sets the snippet {@link MatchRange} corresponding to the given entry. */ 627 @CanIgnoreReturnValue 628 @NonNull setSnippetRange(@onNull MatchRange matchRange)629 public Builder setSnippetRange(@NonNull MatchRange matchRange) { 630 mSnippetRange = Objects.requireNonNull(matchRange); 631 return this; 632 } 633 634 /** Constructs a new {@link MatchInfo}. */ 635 @NonNull build()636 public MatchInfo build() { 637 Bundle bundle = new Bundle(); 638 bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, mPropertyPath); 639 bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_LOWER_FIELD, mExactMatchRange.getStart()); 640 bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_UPPER_FIELD, mExactMatchRange.getEnd()); 641 if (mSubmatchRange != null) { 642 // Only populate the submatch fields if it was actually set. 643 bundle.putInt(MatchInfo.SUBMATCH_RANGE_LOWER_FIELD, mSubmatchRange.getStart()); 644 } 645 646 if (mSubmatchRange != null) { 647 // Only populate the submatch fields if it was actually set. 648 // Moved to separate block for Nullness Checker. 649 bundle.putInt(MatchInfo.SUBMATCH_RANGE_UPPER_FIELD, mSubmatchRange.getEnd()); 650 } 651 652 bundle.putInt(MatchInfo.SNIPPET_RANGE_LOWER_FIELD, mSnippetRange.getStart()); 653 bundle.putInt(MatchInfo.SNIPPET_RANGE_UPPER_FIELD, mSnippetRange.getEnd()); 654 return new MatchInfo(bundle, /*document=*/ null); 655 } 656 } 657 } 658 659 /** 660 * Class providing the position range of matching information. 661 * 662 * <p>All ranges are finite, and the left side of the range is always {@code <=} the right side 663 * of the range. 664 * 665 * <p>Example: MatchRange(0, 100) represents hundred ints from 0 to 99." 666 */ 667 public static final class MatchRange { 668 private final int mEnd; 669 private final int mStart; 670 671 /** 672 * Creates a new immutable range. 673 * 674 * <p>The endpoints are {@code [start, end)}; that is the range is bounded. {@code start} 675 * must be lesser or equal to {@code end}. 676 * 677 * @param start The start point (inclusive) 678 * @param end The end point (exclusive) 679 */ MatchRange(int start, int end)680 public MatchRange(int start, int end) { 681 if (start > end) { 682 throw new IllegalArgumentException( 683 "Start point must be less than or equal to " + "end point"); 684 } 685 mStart = start; 686 mEnd = end; 687 } 688 689 /** Gets the start point (inclusive). */ getStart()690 public int getStart() { 691 return mStart; 692 } 693 694 /** Gets the end point (exclusive). */ getEnd()695 public int getEnd() { 696 return mEnd; 697 } 698 699 @Override equals(@ullable Object other)700 public boolean equals(@Nullable Object other) { 701 if (this == other) { 702 return true; 703 } 704 if (!(other instanceof MatchRange)) { 705 return false; 706 } 707 MatchRange otherMatchRange = (MatchRange) other; 708 return this.getStart() == otherMatchRange.getStart() 709 && this.getEnd() == otherMatchRange.getEnd(); 710 } 711 712 @Override 713 @NonNull toString()714 public String toString() { 715 return "MatchRange { start: " + mStart + " , end: " + mEnd + "}"; 716 } 717 718 @Override hashCode()719 public int hashCode() { 720 return Objects.hash(mStart, mEnd); 721 } 722 } 723 } 724