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