• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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