| /* |
| * Copyright 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.app.appsearch; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.appsearch.annotation.CanIgnoreReturnValue; |
| import android.os.Bundle; |
| |
| import com.android.internal.util.Preconditions; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| |
| /** |
| * This class represents one of the results obtained from an AppSearch query. |
| * |
| * <p>This allows clients to obtain: |
| * |
| * <ul> |
| * <li>The document which matched, using {@link #getGenericDocument} |
| * <li>Information about which properties in the document matched, and "snippet" information |
| * containing textual summaries of the document's matches, using {@link #getMatchInfos} |
| * </ul> |
| * |
| * <p>"Snippet" refers to a substring of text from the content of document that is returned as a |
| * part of search result. |
| * |
| * @see SearchResults |
| */ |
| public final class SearchResult { |
| static final String DOCUMENT_FIELD = "document"; |
| static final String MATCH_INFOS_FIELD = "matchInfos"; |
| static final String PACKAGE_NAME_FIELD = "packageName"; |
| static final String DATABASE_NAME_FIELD = "databaseName"; |
| static final String RANKING_SIGNAL_FIELD = "rankingSignal"; |
| static final String JOINED_RESULTS = "joinedResults"; |
| |
| @NonNull private final Bundle mBundle; |
| |
| /** Cache of the inflated document. Comes from inflating mDocumentBundle at first use. */ |
| @Nullable private GenericDocument mDocument; |
| |
| /** Cache of the inflated matches. Comes from inflating mMatchBundles at first use. */ |
| @Nullable private List<MatchInfo> mMatchInfos; |
| |
| /** @hide */ |
| public SearchResult(@NonNull Bundle bundle) { |
| mBundle = Objects.requireNonNull(bundle); |
| } |
| |
| /** @hide */ |
| @NonNull |
| public Bundle getBundle() { |
| return mBundle; |
| } |
| |
| /** |
| * Contains the matching {@link GenericDocument}. |
| * |
| * @return Document object which matched the query. |
| */ |
| @NonNull |
| public GenericDocument getGenericDocument() { |
| if (mDocument == null) { |
| mDocument = |
| new GenericDocument(Objects.requireNonNull(mBundle.getBundle(DOCUMENT_FIELD))); |
| } |
| return mDocument; |
| } |
| |
| /** |
| * Returns a list of {@link MatchInfo}s providing information about how the document in {@link |
| * #getGenericDocument} matched the query. |
| * |
| * @return List of matches based on {@link SearchSpec}. If snippeting is disabled using {@link |
| * SearchSpec.Builder#setSnippetCount} or {@link |
| * SearchSpec.Builder#setSnippetCountPerProperty}, for all results after that value, this |
| * method returns an empty list. |
| */ |
| @NonNull |
| @SuppressWarnings("deprecation") |
| public List<MatchInfo> getMatchInfos() { |
| if (mMatchInfos == null) { |
| List<Bundle> matchBundles = |
| Objects.requireNonNull(mBundle.getParcelableArrayList(MATCH_INFOS_FIELD)); |
| mMatchInfos = new ArrayList<>(matchBundles.size()); |
| for (int i = 0; i < matchBundles.size(); i++) { |
| MatchInfo matchInfo = new MatchInfo(matchBundles.get(i), getGenericDocument()); |
| if (mMatchInfos != null) { |
| // This additional check is added for NullnessChecker. |
| mMatchInfos.add(matchInfo); |
| } |
| } |
| } |
| // This check is added for NullnessChecker, mMatchInfos will always be NonNull. |
| return Objects.requireNonNull(mMatchInfos); |
| } |
| |
| /** |
| * Contains the package name of the app that stored the {@link GenericDocument}. |
| * |
| * @return Package name that stored the document |
| */ |
| @NonNull |
| public String getPackageName() { |
| return Objects.requireNonNull(mBundle.getString(PACKAGE_NAME_FIELD)); |
| } |
| |
| /** |
| * Contains the database name that stored the {@link GenericDocument}. |
| * |
| * @return Name of the database within which the document is stored |
| */ |
| @NonNull |
| public String getDatabaseName() { |
| return Objects.requireNonNull(mBundle.getString(DATABASE_NAME_FIELD)); |
| } |
| |
| /** |
| * Returns the ranking signal of the {@link GenericDocument}, according to the ranking strategy |
| * set in {@link SearchSpec.Builder#setRankingStrategy(int)}. |
| * |
| * <p>The meaning of the ranking signal and its value is determined by the selected ranking |
| * strategy: |
| * |
| * <ul> |
| * <li>{@link SearchSpec#RANKING_STRATEGY_NONE} - this value will be 0 |
| * <li>{@link SearchSpec#RANKING_STRATEGY_DOCUMENT_SCORE} - the value returned by calling |
| * {@link GenericDocument#getScore()} on the document returned by {@link |
| * #getGenericDocument()} |
| * <li>{@link SearchSpec#RANKING_STRATEGY_CREATION_TIMESTAMP} - the value returned by calling |
| * {@link GenericDocument#getCreationTimestampMillis()} on the document returned by {@link |
| * #getGenericDocument()} |
| * <li>{@link SearchSpec#RANKING_STRATEGY_RELEVANCE_SCORE} - an arbitrary double value where a |
| * higher value means more relevant |
| * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} - the number of times usage has been |
| * reported for the document returned by {@link #getGenericDocument()} |
| * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} - the timestamp of the |
| * most recent usage that has been reported for the document returned by {@link |
| * #getGenericDocument()} |
| * </ul> |
| * |
| * @return Ranking signal of the document |
| */ |
| public double getRankingSignal() { |
| return mBundle.getDouble(RANKING_SIGNAL_FIELD); |
| } |
| |
| /** |
| * Gets a list of {@link SearchResult} joined from the join operation. |
| * |
| * <p>These joined documents match the outer document as specified in the {@link JoinSpec} with |
| * parentPropertyExpression and childPropertyExpression. They are ordered according to the |
| * {@link JoinSpec#getNestedSearchSpec}, and as many SearchResults as specified by {@link |
| * JoinSpec#getMaxJoinedResultCount} will be returned. If no {@link JoinSpec} was specified, |
| * this returns an empty list. |
| * |
| * <p>This method is inefficient to call repeatedly, as new {@link SearchResult} objects are |
| * created each time. |
| * |
| * @return a List of SearchResults containing joined documents. |
| */ |
| @NonNull |
| @SuppressWarnings("deprecation") // Bundle#getParcelableArrayList(String) is deprecated. |
| public List<SearchResult> getJoinedResults() { |
| ArrayList<Bundle> bundles = mBundle.getParcelableArrayList(JOINED_RESULTS); |
| if (bundles == null) { |
| return new ArrayList<>(); |
| } |
| List<SearchResult> res = new ArrayList<>(bundles.size()); |
| for (int i = 0; i < bundles.size(); i++) { |
| res.add(new SearchResult(bundles.get(i))); |
| } |
| |
| return res; |
| } |
| |
| /** Builder for {@link SearchResult} objects. */ |
| public static final class Builder { |
| private final String mPackageName; |
| private final String mDatabaseName; |
| private ArrayList<Bundle> mMatchInfoBundles = new ArrayList<>(); |
| private GenericDocument mGenericDocument; |
| private double mRankingSignal; |
| private ArrayList<Bundle> mJoinedResults = new ArrayList<>(); |
| private boolean mBuilt = false; |
| |
| /** |
| * Constructs a new builder for {@link SearchResult} objects. |
| * |
| * @param packageName the package name the matched document belongs to |
| * @param databaseName the database name the matched document belongs to. |
| */ |
| public Builder(@NonNull String packageName, @NonNull String databaseName) { |
| mPackageName = Objects.requireNonNull(packageName); |
| mDatabaseName = Objects.requireNonNull(databaseName); |
| } |
| |
| /** Sets the document which matched. */ |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder setGenericDocument(@NonNull GenericDocument document) { |
| Objects.requireNonNull(document); |
| resetIfBuilt(); |
| mGenericDocument = document; |
| return this; |
| } |
| |
| /** Adds another match to this SearchResult. */ |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder addMatchInfo(@NonNull MatchInfo matchInfo) { |
| Preconditions.checkState( |
| matchInfo.mDocument == null, |
| "This MatchInfo is already associated with a SearchResult and can't be " |
| + "reassigned"); |
| resetIfBuilt(); |
| mMatchInfoBundles.add(matchInfo.mBundle); |
| return this; |
| } |
| |
| /** Sets the ranking signal of the matched document in this SearchResult. */ |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder setRankingSignal(double rankingSignal) { |
| resetIfBuilt(); |
| mRankingSignal = rankingSignal; |
| return this; |
| } |
| |
| /** |
| * Adds a {@link SearchResult} that was joined by the {@link JoinSpec}. |
| * |
| * @param joinedResult The joined SearchResult to add. |
| */ |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder addJoinedResult(@NonNull SearchResult joinedResult) { |
| resetIfBuilt(); |
| mJoinedResults.add(joinedResult.getBundle()); |
| return this; |
| } |
| |
| /** Constructs a new {@link SearchResult}. */ |
| @NonNull |
| public SearchResult build() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(PACKAGE_NAME_FIELD, mPackageName); |
| bundle.putString(DATABASE_NAME_FIELD, mDatabaseName); |
| bundle.putBundle(DOCUMENT_FIELD, mGenericDocument.getBundle()); |
| bundle.putDouble(RANKING_SIGNAL_FIELD, mRankingSignal); |
| bundle.putParcelableArrayList(MATCH_INFOS_FIELD, mMatchInfoBundles); |
| bundle.putParcelableArrayList(JOINED_RESULTS, mJoinedResults); |
| mBuilt = true; |
| return new SearchResult(bundle); |
| } |
| |
| private void resetIfBuilt() { |
| if (mBuilt) { |
| mMatchInfoBundles = new ArrayList<>(mMatchInfoBundles); |
| mJoinedResults = new ArrayList<>(mJoinedResults); |
| mBuilt = false; |
| } |
| } |
| } |
| |
| /** |
| * This class represents match objects for any Snippets that might be present in {@link |
| * SearchResults} from a query. Using this class, the user can get: |
| * |
| * <ul> |
| * <li>the full text - all of the text in that String property |
| * <li>the exact term match - the 'term' (full word) that matched the query |
| * <li>the subterm match - the portion of the matched term that appears in the query |
| * <li>a suggested text snippet - a portion of the full text surrounding the exact term match, |
| * set to term boundaries. The size of the snippet is specified in {@link |
| * SearchSpec.Builder#setMaxSnippetSize} |
| * </ul> |
| * |
| * for each match in the document. |
| * |
| * <p>Class Example 1: |
| * |
| * <p>A document contains the following text in property "subject": |
| * |
| * <p>"A commonly used fake word is foo. Another nonsense word that’s used a lot is bar." |
| * |
| * <p>If the queryExpression is "foo" and {@link SearchSpec#getMaxSnippetSize} is 10, |
| * |
| * <ul> |
| * <li>{@link MatchInfo#getPropertyPath()} returns "subject" |
| * <li>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another |
| * nonsense word that’s used a lot is bar." |
| * <li>{@link MatchInfo#getExactMatchRange()} returns [29, 32] |
| * <li>{@link MatchInfo#getExactMatch()} returns "foo" |
| * <li>{@link MatchInfo#getSubmatchRange()} returns [29, 32] |
| * <li>{@link MatchInfo#getSubmatch()} returns "foo" |
| * <li>{@link MatchInfo#getSnippetRange()} returns [26, 33] |
| * <li>{@link MatchInfo#getSnippet()} returns "is foo." |
| * </ul> |
| * |
| * <p> |
| * |
| * <p>Class Example 2: |
| * |
| * <p>A document contains one property named "subject" and one property named "sender" which |
| * contains a "name" property. |
| * |
| * <p>In this case, we will have 2 property paths: {@code sender.name} and {@code subject}. |
| * |
| * <p>Let {@code sender.name = "Test Name Jr."} and {@code subject = "Testing 1 2 3"} |
| * |
| * <p>If the queryExpression is "Test" with {@link SearchSpec#TERM_MATCH_PREFIX} and {@link |
| * SearchSpec#getMaxSnippetSize} is 10. We will have 2 matches: |
| * |
| * <p>Match-1 |
| * |
| * <ul> |
| * <li>{@link MatchInfo#getPropertyPath()} returns "sender.name" |
| * <li>{@link MatchInfo#getFullText()} returns "Test Name Jr." |
| * <li>{@link MatchInfo#getExactMatchRange()} returns [0, 4] |
| * <li>{@link MatchInfo#getExactMatch()} returns "Test" |
| * <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4] |
| * <li>{@link MatchInfo#getSubmatch()} returns "Test" |
| * <li>{@link MatchInfo#getSnippetRange()} returns [0, 9] |
| * <li>{@link MatchInfo#getSnippet()} returns "Test Name" |
| * </ul> |
| * |
| * <p>Match-2 |
| * |
| * <ul> |
| * <li>{@link MatchInfo#getPropertyPath()} returns "subject" |
| * <li>{@link MatchInfo#getFullText()} returns "Testing 1 2 3" |
| * <li>{@link MatchInfo#getExactMatchRange()} returns [0, 7] |
| * <li>{@link MatchInfo#getExactMatch()} returns "Testing" |
| * <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4] |
| * <li>{@link MatchInfo#getSubmatch()} returns "Test" |
| * <li>{@link MatchInfo#getSnippetRange()} returns [0, 9] |
| * <li>{@link MatchInfo#getSnippet()} returns "Testing 1" |
| * </ul> |
| */ |
| public static final class MatchInfo { |
| /** The path of the matching snippet property. */ |
| private static final String PROPERTY_PATH_FIELD = "propertyPath"; |
| |
| private static final String EXACT_MATCH_RANGE_LOWER_FIELD = "exactMatchRangeLower"; |
| private static final String EXACT_MATCH_RANGE_UPPER_FIELD = "exactMatchRangeUpper"; |
| private static final String SUBMATCH_RANGE_LOWER_FIELD = "submatchRangeLower"; |
| private static final String SUBMATCH_RANGE_UPPER_FIELD = "submatchRangeUpper"; |
| private static final String SNIPPET_RANGE_LOWER_FIELD = "snippetRangeLower"; |
| private static final String SNIPPET_RANGE_UPPER_FIELD = "snippetRangeUpper"; |
| |
| private final String mPropertyPath; |
| @Nullable private PropertyPath mPropertyPathObject = null; |
| final Bundle mBundle; |
| |
| /** |
| * Document which the match comes from. |
| * |
| * <p>If this is {@code null}, methods which require access to the document, like {@link |
| * #getExactMatch}, will throw {@link NullPointerException}. |
| */ |
| @Nullable final GenericDocument mDocument; |
| |
| /** Full text of the matched property. Populated on first use. */ |
| @Nullable private String mFullText; |
| |
| /** Range of property that exactly matched the query. Populated on first use. */ |
| @Nullable private MatchRange mExactMatchRange; |
| |
| /** |
| * Range of property that corresponds to the subsequence of the exact match that directly |
| * matches a query term. Populated on first use. |
| */ |
| @Nullable private MatchRange mSubmatchRange; |
| |
| /** Range of some reasonable amount of context around the query. Populated on first use. */ |
| @Nullable private MatchRange mWindowRange; |
| |
| MatchInfo(@NonNull Bundle bundle, @Nullable GenericDocument document) { |
| mBundle = Objects.requireNonNull(bundle); |
| mDocument = document; |
| mPropertyPath = Objects.requireNonNull(bundle.getString(PROPERTY_PATH_FIELD)); |
| } |
| |
| /** |
| * Gets the property path corresponding to the given entry. |
| * |
| * <p>A property path is a '.' - delimited sequence of property names indicating which |
| * property in the document these snippets correspond to. |
| * |
| * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. For class |
| * example 1 this returns "subject" |
| */ |
| @NonNull |
| public String getPropertyPath() { |
| return mPropertyPath; |
| } |
| |
| /** |
| * Gets a {@link PropertyPath} object representing the property path corresponding to the |
| * given entry. |
| * |
| * <p>Methods such as {@link GenericDocument#getPropertyDocument} accept a path as a string |
| * rather than a {@link PropertyPath} object. However, you may want to manipulate the path |
| * before getting a property document. This method returns a {@link PropertyPath} rather |
| * than a String for easier path manipulation, which can then be converted to a String. |
| * |
| * @see #getPropertyPath |
| * @see PropertyPath |
| */ |
| @NonNull |
| public PropertyPath getPropertyPathObject() { |
| if (mPropertyPathObject == null) { |
| mPropertyPathObject = new PropertyPath(mPropertyPath); |
| } |
| return mPropertyPathObject; |
| } |
| |
| /** |
| * Gets the full text corresponding to the given entry. |
| * |
| * <p>Class example 1: this returns "A commonly used fake word is foo. Another nonsense word |
| * that's used a lot is bar." |
| * |
| * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name Jr." and, |
| * for the second {@link MatchInfo}, this returns "Testing 1 2 3". |
| */ |
| @NonNull |
| public String getFullText() { |
| if (mFullText == null) { |
| if (mDocument == null) { |
| throw new IllegalStateException( |
| "Document has not been populated; this MatchInfo cannot be used yet"); |
| } |
| mFullText = getPropertyValues(mDocument, mPropertyPath); |
| } |
| return mFullText; |
| } |
| |
| /** |
| * Gets the {@link MatchRange} of the exact term of the given entry that matched the query. |
| * |
| * <p>Class example 1: this returns [29, 32]. |
| * |
| * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 4] and, for the |
| * second {@link MatchInfo}, this returns [0, 7]. |
| */ |
| @NonNull |
| public MatchRange getExactMatchRange() { |
| if (mExactMatchRange == null) { |
| mExactMatchRange = |
| new MatchRange( |
| mBundle.getInt(EXACT_MATCH_RANGE_LOWER_FIELD), |
| mBundle.getInt(EXACT_MATCH_RANGE_UPPER_FIELD)); |
| } |
| return mExactMatchRange; |
| } |
| |
| /** |
| * Gets the exact term of the given entry that matched the query. |
| * |
| * <p>Class example 1: this returns "foo". |
| * |
| * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test" and, for the |
| * second {@link MatchInfo}, this returns "Testing". |
| */ |
| @NonNull |
| public CharSequence getExactMatch() { |
| return getSubstring(getExactMatchRange()); |
| } |
| |
| /** |
| * Gets the {@link MatchRange} of the exact term subsequence of the given entry that matched |
| * the query. |
| * |
| * <p>Class example 1: this returns [29, 32]. |
| * |
| * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 4] and, for the |
| * second {@link MatchInfo}, this returns [0, 4]. |
| */ |
| @NonNull |
| public MatchRange getSubmatchRange() { |
| checkSubmatchSupported(); |
| if (mSubmatchRange == null) { |
| mSubmatchRange = |
| new MatchRange( |
| mBundle.getInt(SUBMATCH_RANGE_LOWER_FIELD), |
| mBundle.getInt(SUBMATCH_RANGE_UPPER_FIELD)); |
| } |
| return mSubmatchRange; |
| } |
| |
| /** |
| * Gets the exact term subsequence of the given entry that matched the query. |
| * |
| * <p>Class example 1: this returns "foo". |
| * |
| * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test" and, for the |
| * second {@link MatchInfo}, this returns "Test". |
| */ |
| @NonNull |
| public CharSequence getSubmatch() { |
| checkSubmatchSupported(); |
| return getSubstring(getSubmatchRange()); |
| } |
| |
| /** |
| * Gets the snippet {@link MatchRange} corresponding to the given entry. |
| * |
| * <p>Only populated when set maxSnippetSize > 0 in {@link |
| * SearchSpec.Builder#setMaxSnippetSize}. |
| * |
| * <p>Class example 1: this returns [29, 41]. |
| * |
| * <p>Class example 2: for the first {@link MatchInfo}, this returns [0, 9] and, for the |
| * second {@link MatchInfo}, this returns [0, 13]. |
| */ |
| @NonNull |
| public MatchRange getSnippetRange() { |
| if (mWindowRange == null) { |
| mWindowRange = |
| new MatchRange( |
| mBundle.getInt(SNIPPET_RANGE_LOWER_FIELD), |
| mBundle.getInt(SNIPPET_RANGE_UPPER_FIELD)); |
| } |
| return mWindowRange; |
| } |
| |
| /** |
| * Gets the snippet corresponding to the given entry. |
| * |
| * <p>Snippet - Provides a subset of the content to display. Only populated when requested |
| * maxSnippetSize > 0. The size of this content can be changed by {@link |
| * SearchSpec.Builder#setMaxSnippetSize}. Windowing is centered around the middle of the |
| * matched token with content on either side clipped to token boundaries. |
| * |
| * <p>Class example 1: this returns "foo. Another". |
| * |
| * <p>Class example 2: for the first {@link MatchInfo}, this returns "Test Name" and, for |
| * the second {@link MatchInfo}, this returns "Testing 1 2 3". |
| */ |
| @NonNull |
| public CharSequence getSnippet() { |
| return getSubstring(getSnippetRange()); |
| } |
| |
| private CharSequence getSubstring(MatchRange range) { |
| return getFullText().substring(range.getStart(), range.getEnd()); |
| } |
| |
| private void checkSubmatchSupported() { |
| if (!mBundle.containsKey(SUBMATCH_RANGE_LOWER_FIELD)) { |
| throw new UnsupportedOperationException( |
| "Submatch is not supported with this backend/Android API level " |
| + "combination"); |
| } |
| } |
| |
| /** Extracts the matching string from the document. */ |
| private static String getPropertyValues(GenericDocument document, String propertyName) { |
| String result = document.getPropertyString(propertyName); |
| if (result == null) { |
| throw new IllegalStateException( |
| "No content found for requested property path: " + propertyName); |
| } |
| return result; |
| } |
| |
| /** Builder for {@link MatchInfo} objects. */ |
| public static final class Builder { |
| private final String mPropertyPath; |
| private MatchRange mExactMatchRange = new MatchRange(0, 0); |
| @Nullable private MatchRange mSubmatchRange; |
| private MatchRange mSnippetRange = new MatchRange(0, 0); |
| |
| /** |
| * Creates a new {@link MatchInfo.Builder} reporting a match with the given property |
| * path. |
| * |
| * <p>A property path is a dot-delimited sequence of property names indicating which |
| * property in the document these snippets correspond to. |
| * |
| * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. For class |
| * example 1 this returns "subject". |
| * |
| * @param propertyPath A dot-delimited sequence of property names indicating which |
| * property in the document these snippets correspond to. |
| */ |
| public Builder(@NonNull String propertyPath) { |
| mPropertyPath = Objects.requireNonNull(propertyPath); |
| } |
| |
| /** Sets the exact {@link MatchRange} corresponding to the given entry. */ |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder setExactMatchRange(@NonNull MatchRange matchRange) { |
| mExactMatchRange = Objects.requireNonNull(matchRange); |
| return this; |
| } |
| |
| /** Sets the submatch {@link MatchRange} corresponding to the given entry. */ |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder setSubmatchRange(@NonNull MatchRange matchRange) { |
| mSubmatchRange = Objects.requireNonNull(matchRange); |
| return this; |
| } |
| |
| /** Sets the snippet {@link MatchRange} corresponding to the given entry. */ |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder setSnippetRange(@NonNull MatchRange matchRange) { |
| mSnippetRange = Objects.requireNonNull(matchRange); |
| return this; |
| } |
| |
| /** Constructs a new {@link MatchInfo}. */ |
| @NonNull |
| public MatchInfo build() { |
| Bundle bundle = new Bundle(); |
| bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, mPropertyPath); |
| bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_LOWER_FIELD, mExactMatchRange.getStart()); |
| bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_UPPER_FIELD, mExactMatchRange.getEnd()); |
| if (mSubmatchRange != null) { |
| // Only populate the submatch fields if it was actually set. |
| bundle.putInt(MatchInfo.SUBMATCH_RANGE_LOWER_FIELD, mSubmatchRange.getStart()); |
| } |
| |
| if (mSubmatchRange != null) { |
| // Only populate the submatch fields if it was actually set. |
| // Moved to separate block for Nullness Checker. |
| bundle.putInt(MatchInfo.SUBMATCH_RANGE_UPPER_FIELD, mSubmatchRange.getEnd()); |
| } |
| |
| bundle.putInt(MatchInfo.SNIPPET_RANGE_LOWER_FIELD, mSnippetRange.getStart()); |
| bundle.putInt(MatchInfo.SNIPPET_RANGE_UPPER_FIELD, mSnippetRange.getEnd()); |
| return new MatchInfo(bundle, /*document=*/ null); |
| } |
| } |
| } |
| |
| /** |
| * Class providing the position range of matching information. |
| * |
| * <p>All ranges are finite, and the left side of the range is always {@code <=} the right side |
| * of the range. |
| * |
| * <p>Example: MatchRange(0, 100) represent a hundred ints from 0 to 99." |
| */ |
| public static final class MatchRange { |
| private final int mEnd; |
| private final int mStart; |
| |
| /** |
| * Creates a new immutable range. |
| * |
| * <p>The endpoints are {@code [start, end)}; that is the range is bounded. {@code start} |
| * must be lesser or equal to {@code end}. |
| * |
| * @param start The start point (inclusive) |
| * @param end The end point (exclusive) |
| */ |
| public MatchRange(int start, int end) { |
| if (start > end) { |
| throw new IllegalArgumentException( |
| "Start point must be less than or equal to " + "end point"); |
| } |
| mStart = start; |
| mEnd = end; |
| } |
| |
| /** Gets the start point (inclusive). */ |
| public int getStart() { |
| return mStart; |
| } |
| |
| /** Gets the end point (exclusive). */ |
| public int getEnd() { |
| return mEnd; |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object other) { |
| if (this == other) { |
| return true; |
| } |
| if (!(other instanceof MatchRange)) { |
| return false; |
| } |
| MatchRange otherMatchRange = (MatchRange) other; |
| return this.getStart() == otherMatchRange.getStart() |
| && this.getEnd() == otherMatchRange.getEnd(); |
| } |
| |
| @Override |
| @NonNull |
| public String toString() { |
| return "MatchRange { start: " + mStart + " , end: " + mEnd + "}"; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mStart, mEnd); |
| } |
| } |
| } |