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