• 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.os.Bundle;
22 
23 import com.android.internal.util.Preconditions;
24 
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Objects;
28 
29 /**
30  * This class represents one of the results obtained from an AppSearch query.
31  *
32  * <p>This allows clients to obtain:
33  *
34  * <ul>
35  *   <li>The document which matched, using {@link #getGenericDocument}
36  *   <li>Information about which properties in the document matched, and "snippet" information
37  *       containing textual summaries of the document's matches, using {@link #getMatchInfos}
38  * </ul>
39  *
40  * <p>"Snippet" refers to a substring of text from the content of document that is returned as a
41  * part of search result.
42  *
43  * @see SearchResults
44  */
45 public final class SearchResult {
46     static final String DOCUMENT_FIELD = "document";
47     static final String MATCH_INFOS_FIELD = "matchInfos";
48     static final String PACKAGE_NAME_FIELD = "packageName";
49     static final String DATABASE_NAME_FIELD = "databaseName";
50     static final String RANKING_SIGNAL_FIELD = "rankingSignal";
51 
52     @NonNull private final Bundle mBundle;
53 
54     /** Cache of the inflated document. Comes from inflating mDocumentBundle at first use. */
55     @Nullable private GenericDocument mDocument;
56 
57     /** Cache of the inflated matches. Comes from inflating mMatchBundles at first use. */
58     @Nullable private List<MatchInfo> mMatchInfos;
59 
60     /** @hide */
SearchResult(@onNull Bundle bundle)61     public SearchResult(@NonNull Bundle bundle) {
62         mBundle = Objects.requireNonNull(bundle);
63     }
64 
65     /** @hide */
66     @NonNull
getBundle()67     public Bundle getBundle() {
68         return mBundle;
69     }
70 
71     /**
72      * Contains the matching {@link GenericDocument}.
73      *
74      * @return Document object which matched the query.
75      */
76     @NonNull
getGenericDocument()77     public GenericDocument getGenericDocument() {
78         if (mDocument == null) {
79             mDocument =
80                     new GenericDocument(Objects.requireNonNull(mBundle.getBundle(DOCUMENT_FIELD)));
81         }
82         return mDocument;
83     }
84 
85     /**
86      * Returns a list of {@link MatchInfo}s providing information about how the document in {@link
87      * #getGenericDocument} matched the query.
88      *
89      * @return List of matches based on {@link SearchSpec}. If snippeting is disabled using {@link
90      *     SearchSpec.Builder#setSnippetCount} or {@link
91      *     SearchSpec.Builder#setSnippetCountPerProperty}, for all results after that value, this
92      *     method returns an empty list.
93      */
94     @NonNull
getMatchInfos()95     public List<MatchInfo> getMatchInfos() {
96         if (mMatchInfos == null) {
97             List<Bundle> matchBundles =
98                     Objects.requireNonNull(mBundle.getParcelableArrayList(MATCH_INFOS_FIELD));
99             mMatchInfos = new ArrayList<>(matchBundles.size());
100             for (int i = 0; i < matchBundles.size(); i++) {
101                 MatchInfo matchInfo = new MatchInfo(matchBundles.get(i), getGenericDocument());
102                 mMatchInfos.add(matchInfo);
103             }
104         }
105         return mMatchInfos;
106     }
107 
108     /**
109      * Contains the package name of the app that stored the {@link GenericDocument}.
110      *
111      * @return Package name that stored the document
112      */
113     @NonNull
getPackageName()114     public String getPackageName() {
115         return Objects.requireNonNull(mBundle.getString(PACKAGE_NAME_FIELD));
116     }
117 
118     /**
119      * Contains the database name that stored the {@link GenericDocument}.
120      *
121      * @return Name of the database within which the document is stored
122      */
123     @NonNull
getDatabaseName()124     public String getDatabaseName() {
125         return Objects.requireNonNull(mBundle.getString(DATABASE_NAME_FIELD));
126     }
127 
128     /**
129      * Returns the ranking signal of the {@link GenericDocument}, according to the ranking strategy
130      * set in {@link SearchSpec.Builder#setRankingStrategy(int)}.
131      *
132      * <p>The meaning of the ranking signal and its value is determined by the selected ranking
133      * strategy:
134      *
135      * <ul>
136      *   <li>{@link SearchSpec#RANKING_STRATEGY_NONE} - this value will be 0
137      *   <li>{@link SearchSpec#RANKING_STRATEGY_DOCUMENT_SCORE} - the value returned by calling
138      *       {@link GenericDocument#getScore()} on the document returned by {@link
139      *       #getGenericDocument()}
140      *   <li>{@link SearchSpec#RANKING_STRATEGY_CREATION_TIMESTAMP} - the value returned by calling
141      *       {@link GenericDocument#getCreationTimestampMillis()} on the document returned by {@link
142      *       #getGenericDocument()}
143      *   <li>{@link SearchSpec#RANKING_STRATEGY_RELEVANCE_SCORE} - an arbitrary double value where a
144      *       higher value means more relevant
145      *   <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} - the number of times usage has been
146      *       reported for the document returned by {@link #getGenericDocument()}
147      *   <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} - the timestamp of the
148      *       most recent usage that has been reported for the document returned by {@link
149      *       #getGenericDocument()}
150      * </ul>
151      *
152      * @return Ranking signal of the document
153      */
getRankingSignal()154     public double getRankingSignal() {
155         return mBundle.getDouble(RANKING_SIGNAL_FIELD);
156     }
157 
158     /** Builder for {@link SearchResult} objects. */
159     public static final class Builder {
160         private final String mPackageName;
161         private final String mDatabaseName;
162         private ArrayList<Bundle> mMatchInfoBundles = new ArrayList<>();
163         private GenericDocument mGenericDocument;
164         private double mRankingSignal;
165         private boolean mBuilt = false;
166 
167         /**
168          * Constructs a new builder for {@link SearchResult} objects.
169          *
170          * @param packageName the package name the matched document belongs to
171          * @param databaseName the database name the matched document belongs to.
172          */
Builder(@onNull String packageName, @NonNull String databaseName)173         public Builder(@NonNull String packageName, @NonNull String databaseName) {
174             mPackageName = Objects.requireNonNull(packageName);
175             mDatabaseName = Objects.requireNonNull(databaseName);
176         }
177 
178         /** Sets the document which matched. */
179         @NonNull
setGenericDocument(@onNull GenericDocument document)180         public Builder setGenericDocument(@NonNull GenericDocument document) {
181             Objects.requireNonNull(document);
182             resetIfBuilt();
183             mGenericDocument = document;
184             return this;
185         }
186 
187         /** Adds another match to this SearchResult. */
188         @NonNull
addMatchInfo(@onNull MatchInfo matchInfo)189         public Builder addMatchInfo(@NonNull MatchInfo matchInfo) {
190             Preconditions.checkState(
191                     matchInfo.mDocument == null,
192                     "This MatchInfo is already associated with a SearchResult and can't be "
193                             + "reassigned");
194             resetIfBuilt();
195             mMatchInfoBundles.add(matchInfo.mBundle);
196             return this;
197         }
198 
199         /** Sets the ranking signal of the matched document in this SearchResult. */
200         @NonNull
setRankingSignal(double rankingSignal)201         public Builder setRankingSignal(double rankingSignal) {
202             resetIfBuilt();
203             mRankingSignal = rankingSignal;
204             return this;
205         }
206 
207         /** Constructs a new {@link SearchResult}. */
208         @NonNull
build()209         public SearchResult build() {
210             Bundle bundle = new Bundle();
211             bundle.putString(PACKAGE_NAME_FIELD, mPackageName);
212             bundle.putString(DATABASE_NAME_FIELD, mDatabaseName);
213             bundle.putBundle(DOCUMENT_FIELD, mGenericDocument.getBundle());
214             bundle.putDouble(RANKING_SIGNAL_FIELD, mRankingSignal);
215             bundle.putParcelableArrayList(MATCH_INFOS_FIELD, mMatchInfoBundles);
216             mBuilt = true;
217             return new SearchResult(bundle);
218         }
219 
resetIfBuilt()220         private void resetIfBuilt() {
221             if (mBuilt) {
222                 mMatchInfoBundles = new ArrayList<>(mMatchInfoBundles);
223                 mBuilt = false;
224             }
225         }
226     }
227 
228     /**
229      * This class represents a match objects for any Snippets that might be present in {@link
230      * SearchResults} from query. Using this class user can get the full text, exact matches and
231      * Snippets of document content for a given match.
232      *
233      * <p>Class Example 1: A document contains following text in property subject:
234      *
235      * <p>A commonly used fake word is foo. Another nonsense word that’s used a lot is bar.
236      *
237      * <p>If the queryExpression is "foo".
238      *
239      * <p>{@link MatchInfo#getPropertyPath()} returns "subject"
240      *
241      * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
242      * nonsense word that’s used a lot is bar."
243      *
244      * <p>{@link MatchInfo#getExactMatchRange()} returns [29, 32]
245      *
246      * <p>{@link MatchInfo#getExactMatch()} returns "foo"
247      *
248      * <p>{@link MatchInfo#getSnippetRange()} returns [26, 33]
249      *
250      * <p>{@link MatchInfo#getSnippet()} returns "is foo."
251      *
252      * <p>
253      *
254      * <p>Class Example 2: A document contains a property name sender which contains 2 property
255      * names name and email, so we will have 2 property paths: {@code sender.name} and {@code
256      * sender.email}.
257      *
258      * <p>Let {@code sender.name = "Test Name Jr."} and {@code sender.email =
259      * "TestNameJr@gmail.com"}
260      *
261      * <p>If the queryExpression is "Test". We will have 2 matches.
262      *
263      * <p>Match-1
264      *
265      * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
266      *
267      * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
268      *
269      * <p>{@link MatchInfo#getExactMatchRange()} returns [0, 4]
270      *
271      * <p>{@link MatchInfo#getExactMatch()} returns "Test"
272      *
273      * <p>{@link MatchInfo#getSnippetRange()} returns [0, 9]
274      *
275      * <p>{@link MatchInfo#getSnippet()} returns "Test Name"
276      *
277      * <p>Match-2
278      *
279      * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
280      *
281      * <p>{@link MatchInfo#getFullText()} returns "TestNameJr@gmail.com"
282      *
283      * <p>{@link MatchInfo#getExactMatchRange()} returns [0, 20]
284      *
285      * <p>{@link MatchInfo#getExactMatch()} returns "TestNameJr@gmail.com"
286      *
287      * <p>{@link MatchInfo#getSnippetRange()} returns [0, 20]
288      *
289      * <p>{@link MatchInfo#getSnippet()} returns "TestNameJr@gmail.com"
290      */
291     public static final class MatchInfo {
292         /** The path of the matching snippet property. */
293         private static final String PROPERTY_PATH_FIELD = "propertyPath";
294 
295         private static final String EXACT_MATCH_RANGE_LOWER_FIELD = "exactMatchRangeLower";
296         private static final String EXACT_MATCH_RANGE_UPPER_FIELD = "exactMatchRangeUpper";
297         private static final String SNIPPET_RANGE_LOWER_FIELD = "snippetRangeLower";
298         private static final String SNIPPET_RANGE_UPPER_FIELD = "snippetRangeUpper";
299 
300         private final String mPropertyPath;
301         final Bundle mBundle;
302 
303         /**
304          * Document which the match comes from.
305          *
306          * <p>If this is {@code null}, methods which require access to the document, like {@link
307          * #getExactMatch}, will throw {@link NullPointerException}.
308          */
309         @Nullable final GenericDocument mDocument;
310 
311         /** Full text of the matched property. Populated on first use. */
312         @Nullable private String mFullText;
313 
314         /** Range of property that exactly matched the query. Populated on first use. */
315         @Nullable private MatchRange mExactMatchRange;
316 
317         /** Range of some reasonable amount of context around the query. Populated on first use. */
318         @Nullable private MatchRange mWindowRange;
319 
MatchInfo(@onNull Bundle bundle, @Nullable GenericDocument document)320         MatchInfo(@NonNull Bundle bundle, @Nullable GenericDocument document) {
321             mBundle = Objects.requireNonNull(bundle);
322             mDocument = document;
323             mPropertyPath = Objects.requireNonNull(bundle.getString(PROPERTY_PATH_FIELD));
324         }
325 
326         /**
327          * Gets the property path corresponding to the given entry.
328          *
329          * <p>A property path is a '.' - delimited sequence of property names indicating which
330          * property in the document these snippets correspond to.
331          *
332          * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. For class
333          * example 1 this returns "subject"
334          */
335         @NonNull
getPropertyPath()336         public String getPropertyPath() {
337             return mPropertyPath;
338         }
339 
340         /**
341          * Gets the full text corresponding to the given entry.
342          *
343          * <p>For class example this returns "A commonly used fake word is foo. Another nonsense
344          * word that's used a lot is bar."
345          */
346         @NonNull
getFullText()347         public String getFullText() {
348             if (mFullText == null) {
349                 Preconditions.checkState(
350                         mDocument != null,
351                         "Document has not been populated; this MatchInfo cannot be used yet");
352                 mFullText = getPropertyValues(mDocument, mPropertyPath);
353             }
354             return mFullText;
355         }
356 
357         /**
358          * Gets the exact {@link MatchRange} corresponding to the given entry.
359          *
360          * <p>For class example 1 this returns [29, 32]
361          */
362         @NonNull
getExactMatchRange()363         public MatchRange getExactMatchRange() {
364             if (mExactMatchRange == null) {
365                 mExactMatchRange =
366                         new MatchRange(
367                                 mBundle.getInt(EXACT_MATCH_RANGE_LOWER_FIELD),
368                                 mBundle.getInt(EXACT_MATCH_RANGE_UPPER_FIELD));
369             }
370             return mExactMatchRange;
371         }
372 
373         /**
374          * Gets the {@link MatchRange} corresponding to the given entry.
375          *
376          * <p>For class example 1 this returns "foo"
377          */
378         @NonNull
getExactMatch()379         public CharSequence getExactMatch() {
380             return getSubstring(getExactMatchRange());
381         }
382 
383         /**
384          * Gets the snippet {@link MatchRange} corresponding to the given entry.
385          *
386          * <p>Only populated when set maxSnippetSize > 0 in {@link
387          * SearchSpec.Builder#setMaxSnippetSize}.
388          *
389          * <p>For class example 1 this returns [29, 41].
390          */
391         @NonNull
getSnippetRange()392         public MatchRange getSnippetRange() {
393             if (mWindowRange == null) {
394                 mWindowRange =
395                         new MatchRange(
396                                 mBundle.getInt(SNIPPET_RANGE_LOWER_FIELD),
397                                 mBundle.getInt(SNIPPET_RANGE_UPPER_FIELD));
398             }
399             return mWindowRange;
400         }
401 
402         /**
403          * Gets the snippet corresponding to the given entry.
404          *
405          * <p>Snippet - Provides a subset of the content to display. Only populated when requested
406          * maxSnippetSize > 0. The size of this content can be changed by {@link
407          * SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of the
408          * matched token with content on either side clipped to token boundaries.
409          *
410          * <p>For class example 1 this returns "foo. Another"
411          */
412         @NonNull
getSnippet()413         public CharSequence getSnippet() {
414             return getSubstring(getSnippetRange());
415         }
416 
getSubstring(MatchRange range)417         private CharSequence getSubstring(MatchRange range) {
418             return getFullText().substring(range.getStart(), range.getEnd());
419         }
420 
421         /** Extracts the matching string from the document. */
getPropertyValues(GenericDocument document, String propertyName)422         private static String getPropertyValues(GenericDocument document, String propertyName) {
423             // In IcingLib snippeting is available for only 3 data types i.e String, double and
424             // long, so we need to check which of these three are requested.
425             // TODO (tytytyww): support double[] and long[].
426             String result = document.getPropertyString(propertyName);
427             if (result == null) {
428                 throw new IllegalStateException(
429                         "No content found for requested property path: " + propertyName);
430             }
431             return result;
432         }
433 
434         /** Builder for {@link MatchInfo} objects. */
435         public static final class Builder {
436             private final String mPropertyPath;
437             private MatchRange mExactMatchRange = new MatchRange(0, 0);
438             private MatchRange mSnippetRange = new MatchRange(0, 0);
439 
440             /**
441              * Creates a new {@link MatchInfo.Builder} reporting a match with the given property
442              * path.
443              *
444              * <p>A property path is a dot-delimited sequence of property names indicating which
445              * property in the document these snippets correspond to.
446              *
447              * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
448              * For class example 1 this returns "subject".
449              *
450              * @param propertyPath A {@code dot-delimited sequence of property names indicating
451              *                     which property in the document these snippets correspond to.
452              */
Builder(@onNull String propertyPath)453             public Builder(@NonNull String propertyPath) {
454                 mPropertyPath = Objects.requireNonNull(propertyPath);
455             }
456 
457             /** Sets the exact {@link MatchRange} corresponding to the given entry. */
458             @NonNull
setExactMatchRange(@onNull MatchRange matchRange)459             public Builder setExactMatchRange(@NonNull MatchRange matchRange) {
460                 mExactMatchRange = Objects.requireNonNull(matchRange);
461                 return this;
462             }
463 
464             /** Sets the snippet {@link MatchRange} corresponding to the given entry. */
465             @NonNull
setSnippetRange(@onNull MatchRange matchRange)466             public Builder setSnippetRange(@NonNull MatchRange matchRange) {
467                 mSnippetRange = Objects.requireNonNull(matchRange);
468                 return this;
469             }
470 
471             /** Constructs a new {@link MatchInfo}. */
472             @NonNull
build()473             public MatchInfo build() {
474                 Bundle bundle = new Bundle();
475                 bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, mPropertyPath);
476                 bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_LOWER_FIELD, mExactMatchRange.getStart());
477                 bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_UPPER_FIELD, mExactMatchRange.getEnd());
478                 bundle.putInt(MatchInfo.SNIPPET_RANGE_LOWER_FIELD, mSnippetRange.getStart());
479                 bundle.putInt(MatchInfo.SNIPPET_RANGE_UPPER_FIELD, mSnippetRange.getEnd());
480                 return new MatchInfo(bundle, /*document=*/ null);
481             }
482         }
483     }
484 
485     /**
486      * Class providing the position range of matching information.
487      *
488      * <p>All ranges are finite, and the left side of the range is always {@code <=} the right side
489      * of the range.
490      *
491      * <p>Example: MatchRange(0, 100) represent a hundred ints from 0 to 99."
492      */
493     public static final class MatchRange {
494         private final int mEnd;
495         private final int mStart;
496 
497         /**
498          * Creates a new immutable range.
499          *
500          * <p>The endpoints are {@code [start, end)}; that is the range is bounded. {@code start}
501          * must be lesser or equal to {@code end}.
502          *
503          * @param start The start point (inclusive)
504          * @param end The end point (exclusive)
505          */
MatchRange(int start, int end)506         public MatchRange(int start, int end) {
507             if (start > end) {
508                 throw new IllegalArgumentException(
509                         "Start point must be less than or equal to " + "end point");
510             }
511             mStart = start;
512             mEnd = end;
513         }
514 
515         /** Gets the start point (inclusive). */
getStart()516         public int getStart() {
517             return mStart;
518         }
519 
520         /** Gets the end point (exclusive). */
getEnd()521         public int getEnd() {
522             return mEnd;
523         }
524 
525         @Override
equals(@ullable Object other)526         public boolean equals(@Nullable Object other) {
527             if (this == other) {
528                 return true;
529             }
530             if (!(other instanceof MatchRange)) {
531                 return false;
532             }
533             MatchRange otherMatchRange = (MatchRange) other;
534             return this.getStart() == otherMatchRange.getStart()
535                     && this.getEnd() == otherMatchRange.getEnd();
536         }
537 
538         @Override
539         @NonNull
toString()540         public String toString() {
541             return "MatchRange { start: " + mStart + " , end: " + mEnd + "}";
542         }
543 
544         @Override
hashCode()545         public int hashCode() {
546             return Objects.hash(mStart, mEnd);
547         }
548     }
549 }
550