public final class

SearchResult

extends AbstractSafeParcelable

 java.lang.Object

androidx.appsearch.safeparcel.AbstractSafeParcelable

↳androidx.appsearch.app.SearchResult

Gradle dependencies

compile group: 'androidx.appsearch', name: 'appsearch', version: '1.1.0-alpha05'

  • groupId: androidx.appsearch
  • artifactId: appsearch
  • version: 1.1.0-alpha05

Artifact androidx.appsearch:appsearch:1.1.0-alpha05 it located at Google repository (https://maven.google.com/)

Overview

This class represents one of the results obtained from an AppSearch query.

This allows clients to obtain:

"Snippet" refers to a substring of text from the content of document that is returned as a part of search result.

Summary

Fields
public static final <any>CREATOR

Methods
public java.lang.StringgetDatabaseName()

Contains the database name that stored the GenericDocument.

public java.lang.ObjectgetDocument(java.lang.Class<java.lang.Object> documentClass)

Contains the matching document, converted to the given document class.

public java.lang.ObjectgetDocument(java.lang.Class<java.lang.Object> documentClass, java.util.Map<java.lang.String, java.util.List> documentClassMap)

Contains the matching document, converted to the given document class.

public GenericDocumentgetGenericDocument()

Contains the matching GenericDocument.

public java.util.List<java.lang.Double>getInformationalRankingSignals()

Returns the informational ranking signals of the GenericDocument, according to the expressions added in SearchSpec.Builder.addInformationalRankingExpressions(String...).

public java.util.List<SearchResult>getJoinedResults()

Gets a list of SearchResult joined from the join operation.

public java.util.List<SearchResult.MatchInfo>getMatchInfos()

Returns a list of SearchResult.MatchInfos providing information about how the document in SearchResult.getGenericDocument() matched the query.

public java.lang.StringgetPackageName()

Contains the package name of the app that stored the GenericDocument.

public doublegetRankingSignal()

Returns the ranking signal of the GenericDocument, according to the ranking strategy set in SearchSpec.Builder.setRankingStrategy(int).

public voidwriteToParcel(Parcel dest, int flags)

To be implemented by child classes.

from AbstractSafeParcelabledescribeContents
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final <any> CREATOR

Methods

public java.lang.Object getDocument(java.lang.Class<java.lang.Object> documentClass)

Contains the matching document, converted to the given document class.

This is equivalent to calling getGenericDocument().toDocumentClass(T.class).

Parameters:

documentClass: the document class to be passed to GenericDocument.toDocumentClass(Class).

Returns:

Document object which matched the query.

See also: GenericDocument.toDocumentClass(Class)

public java.lang.Object getDocument(java.lang.Class<java.lang.Object> documentClass, java.util.Map<java.lang.String, java.util.List> documentClassMap)

Contains the matching document, converted to the given document class.

This is equivalent to calling getGenericDocument().toDocumentClass(T.class, documentClassMap).

Parameters:

documentClass: the document class to be passed to GenericDocument.toDocumentClass(Class, Map>).
documentClassMap: the document class map to be passed to GenericDocument.toDocumentClass(Class, Map>).

Returns:

Document object which matched the query.

See also: GenericDocument.toDocumentClass(Class, Map>)

public GenericDocument getGenericDocument()

Contains the matching GenericDocument.

Returns:

Document object which matched the query.

public java.util.List<SearchResult.MatchInfo> getMatchInfos()

Returns a list of SearchResult.MatchInfos providing information about how the document in SearchResult.getGenericDocument() matched the query.

Returns:

List of matches based on SearchSpec. If snippeting is disabled using SearchSpec.Builder.setSnippetCount(int) or SearchSpec.Builder.setSnippetCountPerProperty(int), for all results after that value, this method returns an empty list.

public java.lang.String getPackageName()

Contains the package name of the app that stored the GenericDocument.

Returns:

Package name that stored the document

public java.lang.String getDatabaseName()

Contains the database name that stored the GenericDocument.

Returns:

Name of the database within which the document is stored

public double getRankingSignal()

Returns the ranking signal of the GenericDocument, according to the ranking strategy set in SearchSpec.Builder.setRankingStrategy(int). The meaning of the ranking signal and its value is determined by the selected ranking strategy:

Returns:

Ranking signal of the document

public java.util.List<java.lang.Double> getInformationalRankingSignals()

Returns the informational ranking signals of the GenericDocument, according to the expressions added in SearchSpec.Builder.addInformationalRankingExpressions(String...).

public java.util.List<SearchResult> getJoinedResults()

Gets a list of SearchResult joined from the join operation.

These joined documents match the outer document as specified in the JoinSpec with parentPropertyExpression and childPropertyExpression. They are ordered according to the JoinSpec.getNestedSearchSpec(), and as many SearchResults as specified by JoinSpec.getMaxJoinedResultCount() will be returned. If no JoinSpec was specified, this returns an empty list.

This method is inefficient to call repeatedly, as new SearchResult objects are created each time.

Returns:

a List of SearchResults containing joined documents.

public void writeToParcel(Parcel dest, int flags)

To be implemented by child classes.

This is purely for code sync purpose. Have writeToParcel here so we can keep "@Override" in child classes.

Source

/*
 * 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 androidx.appsearch.app;

import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
import androidx.appsearch.annotation.CanIgnoreReturnValue;
import androidx.appsearch.exceptions.AppSearchException;
import androidx.appsearch.flags.FlaggedApi;
import androidx.appsearch.flags.Flags;
import androidx.appsearch.safeparcel.AbstractSafeParcelable;
import androidx.appsearch.safeparcel.GenericDocumentParcel;
import androidx.appsearch.safeparcel.SafeParcelable;
import androidx.appsearch.safeparcel.stub.StubCreators.MatchInfoCreator;
import androidx.appsearch.safeparcel.stub.StubCreators.SearchResultCreator;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * 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
 */
@SafeParcelable.Class(creator = "SearchResultCreator")
@SuppressWarnings("HiddenSuperclass")
public final class SearchResult extends AbstractSafeParcelable {
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    @NonNull public static final Parcelable.Creator<SearchResult> CREATOR =
            new SearchResultCreator();

    @Field(id = 1)
    final GenericDocumentParcel mDocument;
    @Field(id = 2)
    final List<MatchInfo> mMatchInfos;
    @Field(id = 3, getter = "getPackageName")
    private final String mPackageName;
    @Field(id = 4, getter = "getDatabaseName")
    private final String mDatabaseName;
    @Field(id = 5, getter = "getRankingSignal")
    private final double mRankingSignal;
    @Field(id = 6, getter = "getJoinedResults")
    private final List<SearchResult> mJoinedResults;
    @NonNull
    @Field(id = 7, getter = "getInformationalRankingSignals")
    private final List<Double> mInformationalRankingSignals;


    /** Cache of the {@link GenericDocument}. Comes from mDocument at first use. */
    @Nullable
    private GenericDocument mDocumentCached;

    /** Cache of the inflated {@link MatchInfo}. Comes from inflating mMatchInfos at first use. */
    @Nullable
    private List<MatchInfo> mMatchInfosCached;

    /** @exportToFramework:hide */
    @Constructor
    SearchResult(
            @Param(id = 1) @NonNull 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) {
        mDocument = Preconditions.checkNotNull(document);
        mMatchInfos = Preconditions.checkNotNull(matchInfos);
        mPackageName = Preconditions.checkNotNull(packageName);
        mDatabaseName = Preconditions.checkNotNull(databaseName);
        mRankingSignal = rankingSignal;
        mJoinedResults = Collections.unmodifiableList(Preconditions.checkNotNull(joinedResults));
        if (informationalRankingSignals != null) {
            mInformationalRankingSignals = Collections.unmodifiableList(
                    informationalRankingSignals);
        } else {
            mInformationalRankingSignals = Collections.emptyList();
        }
    }

// @exportToFramework:startStrip()
    /**
     * Contains the matching document, converted to the given document class.
     *
     * <p>This is equivalent to calling {@code getGenericDocument().toDocumentClass(T.class)}.
     *
     * @param documentClass the document class to be passed to
     *                      {@link GenericDocument#toDocumentClass(Class)}.
     * @return Document object which matched the query.
     * @throws AppSearchException if no factory for this document class could be found on the
     *       classpath.
     * @see GenericDocument#toDocumentClass(Class)
     */
    @NonNull
    public <T> T getDocument(@NonNull java.lang.Class<T> documentClass) throws AppSearchException {
        return getDocument(documentClass, /* documentClassMap= */null);
    }

    /**
     * Contains the matching document, converted to the given document class.
     *
     * <p>This is equivalent to calling {@code getGenericDocument().toDocumentClass(T.class,
     * documentClassMap)}.
     *
     * @param documentClass the document class to be passed to
     *                      {@link GenericDocument#toDocumentClass(Class, Map)}.
     * @param documentClassMap the document class map to be passed to
     *                         {@link GenericDocument#toDocumentClass(Class, Map)}.
     * @return Document object which matched the query.
     * @throws AppSearchException if no factory for this document class could be found on the
     *                            classpath.
     * @see GenericDocument#toDocumentClass(Class, Map)
     */
    @NonNull
    public <T> T getDocument(@NonNull java.lang.Class<T> documentClass,
            @Nullable Map<String, List<String>> documentClassMap) throws AppSearchException {
        Preconditions.checkNotNull(documentClass);
        return getGenericDocument().toDocumentClass(documentClass, documentClassMap);
    }
// @exportToFramework:endStrip()

    /**
     * Contains the matching {@link GenericDocument}.
     *
     * @return Document object which matched the query.
     */
    @NonNull
    public GenericDocument getGenericDocument() {
        if (mDocumentCached == null) {
            mDocumentCached = new GenericDocument(mDocument);
        }
        return mDocumentCached;
    }

    /**
     * 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
    public List<MatchInfo> getMatchInfos() {
        if (mMatchInfosCached == null) {
            mMatchInfosCached = new ArrayList<>(mMatchInfos.size());
            for (int i = 0; i < mMatchInfos.size(); i++) {
                MatchInfo matchInfo = mMatchInfos.get(i);
                matchInfo.setDocument(getGenericDocument());
                if (mMatchInfosCached != null) {
                    // This additional check is added for NullnessChecker.
                    mMatchInfosCached.add(matchInfo);
                }
            }
            mMatchInfosCached = Collections.unmodifiableList(mMatchInfosCached);
        }
        // This check is added for NullnessChecker, mMatchInfos will always be NonNull.
        return Preconditions.checkNotNull(mMatchInfosCached);
    }

    /**
     * Contains the package name of the app that stored the {@link GenericDocument}.
     *
     * @return Package name that stored the document
     */
    @NonNull
    public String getPackageName() {
        return mPackageName;
    }

    /**
     * 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 mDatabaseName;
    }

    /**
     * Returns the ranking signal of the {@link GenericDocument}, according to the
     * ranking strategy set in {@link SearchSpec.Builder#setRankingStrategy(int)}.
     *
     * 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>
     * <li>{@link SearchSpec#RANKING_STRATEGY_DOCUMENT_SCORE} - the value returned by calling
     * {@link GenericDocument#getScore()} on the document returned by
     * {@link #getGenericDocument()}</li>
     * <li>{@link SearchSpec#RANKING_STRATEGY_CREATION_TIMESTAMP} - the value returned by calling
     * {@link GenericDocument#getCreationTimestampMillis()} on the document returned by
     * {@link #getGenericDocument()}</li>
     * <li>{@link SearchSpec#RANKING_STRATEGY_RELEVANCE_SCORE} - an arbitrary double value where
     * a higher value means more relevant</li>
     * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} - the number of times usage has been
     * reported for the document returned by {@link #getGenericDocument()}</li>
     * <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()}</li>
     * </ul>
     *
     * @return Ranking signal of the document
     */
    public double getRankingSignal() {
        return mRankingSignal;
    }

    /**
     * Returns the informational ranking signals of the {@link GenericDocument}, according to the
     * expressions added in {@link SearchSpec.Builder#addInformationalRankingExpressions}.
     */
    @NonNull
    @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
    public List<Double> getInformationalRankingSignals() {
        return mInformationalRankingSignals;
    }

    /**
     * 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
    public List<SearchResult> getJoinedResults() {
        return mJoinedResults;
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        SearchResultCreator.writeToParcel(this, dest, flags);
    }

    /** Builder for {@link SearchResult} objects. */
    public static final class Builder {
        private final String mPackageName;
        private final String mDatabaseName;
        private List<MatchInfo> mMatchInfos = new ArrayList<>();
        private GenericDocument mGenericDocument;
        private double mRankingSignal;
        private List<Double> mInformationalRankingSignals = new ArrayList<>();
        private List<SearchResult> 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 = Preconditions.checkNotNull(packageName);
            mDatabaseName = Preconditions.checkNotNull(databaseName);
        }

        /** @exportToFramework:hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public Builder(@NonNull SearchResult searchResult) {
            Preconditions.checkNotNull(searchResult);
            mPackageName = searchResult.getPackageName();
            mDatabaseName = searchResult.getDatabaseName();
            mGenericDocument = searchResult.getGenericDocument();
            mRankingSignal = searchResult.getRankingSignal();
            mInformationalRankingSignals = new ArrayList<>(
                    searchResult.getInformationalRankingSignals());
            List<MatchInfo> matchInfos = searchResult.getMatchInfos();
            for (int i = 0; i < matchInfos.size(); i++) {
                addMatchInfo(new MatchInfo.Builder(matchInfos.get(i)).build());
            }
            List<SearchResult> joinedResults = searchResult.getJoinedResults();
            for (int i = 0; i < joinedResults.size(); i++) {
                addJoinedResult(joinedResults.get(i));
            }
        }

// @exportToFramework:startStrip()
        /**
         * Sets the document which matched.
         *
         * @param document An instance of a class annotated with
         * {@link androidx.appsearch.annotation.Document}.
         *
         * @throws AppSearchException if an error occurs converting a document class into a
         *                            {@link GenericDocument}.
         */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setDocument(@NonNull Object document) throws AppSearchException {
            Preconditions.checkNotNull(document);
            resetIfBuilt();
            return setGenericDocument(GenericDocument.fromDocumentClass(document));
        }
// @exportToFramework:endStrip()

        /** Sets the document which matched. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setGenericDocument(@NonNull GenericDocument document) {
            Preconditions.checkNotNull(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();
            mMatchInfos.add(matchInfo);
            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 the informational ranking signal of the matched document in this SearchResult. */
        @CanIgnoreReturnValue
        @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
        @NonNull
        public Builder addInformationalRankingSignal(double rankingSignal) {
            resetIfBuilt();
            mInformationalRankingSignals.add(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);
            return this;
        }

        /**
         * Clears the {@link SearchResult}s that were joined.
         *
         * @exportToFramework:hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @CanIgnoreReturnValue
        @NonNull
        public Builder clearJoinedResults() {
            resetIfBuilt();
            mJoinedResults.clear();
            return this;
        }

        /** Constructs a new {@link SearchResult}. */
        @NonNull
        public SearchResult build() {
            mBuilt = true;
            return new SearchResult(
                    mGenericDocument.getDocumentParcel(),
                    mMatchInfos,
                    mPackageName,
                    mDatabaseName,
                    mRankingSignal,
                    mJoinedResults,
                    mInformationalRankingSignals);
        }

        private void resetIfBuilt() {
            if (mBuilt) {
                mMatchInfos = new ArrayList<>(mMatchInfos);
                mJoinedResults = new ArrayList<>(mJoinedResults);
                mInformationalRankingSignals = new ArrayList<>(mInformationalRankingSignals);
                mBuilt = false;
            }
        }
    }

    /**
     * This class represents match objects for any snippets that might be present in
     * {@link SearchResults} from a query. Using this class, you can get:
     * <ul>
     *     <li>the full text - all of the text in that String property</li>
     *     <li>the exact term match - the 'term' (full word) that matched the query</li>
     *     <li>the subterm match - the portion of the matched term that appears in the query</li>
     *     <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}</li>
     * </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>
     *      <li>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
     * nonsense word that’s used a lot is bar."</li>
     *      <li>{@link MatchInfo#getExactMatchRange()} returns [29, 32]</li>
     *      <li>{@link MatchInfo#getExactMatch()} returns "foo"</li>
     *      <li>{@link MatchInfo#getSubmatchRange()} returns [29, 32]</li>
     *      <li>{@link MatchInfo#getSubmatch()} returns "foo"</li>
     *      <li>{@link MatchInfo#getSnippetRange()} returns [26, 33]</li>
     *      <li>{@link MatchInfo#getSnippet()} returns "is foo."</li>
     * </ul>
     * <p>
     * <p>Class Example 2:
     * <p>A document contains one property named "subject" and one property named "sender" which
     * contains a "name" property.
     *
     * 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>
     *      <li>{@link MatchInfo#getFullText()} returns "Test Name Jr."</li>
     *      <li>{@link MatchInfo#getExactMatchRange()} returns [0, 4]</li>
     *      <li>{@link MatchInfo#getExactMatch()} returns "Test"</li>
     *      <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4]</li>
     *      <li>{@link MatchInfo#getSubmatch()} returns "Test"</li>
     *      <li>{@link MatchInfo#getSnippetRange()} returns [0, 9]</li>
     *      <li>{@link MatchInfo#getSnippet()} returns "Test Name"</li>
     * </ul>
     * <p> Match-2
     * <ul>
     *      <li>{@link MatchInfo#getPropertyPath()} returns "subject"</li>
     *      <li>{@link MatchInfo#getFullText()} returns "Testing 1 2 3"</li>
     *      <li>{@link MatchInfo#getExactMatchRange()} returns [0, 7]</li>
     *      <li>{@link MatchInfo#getExactMatch()} returns "Testing"</li>
     *      <li>{@link MatchInfo#getSubmatchRange()} returns [0, 4]</li>
     *      <li>{@link MatchInfo#getSubmatch()} returns "Test"</li>
     *      <li>{@link MatchInfo#getSnippetRange()} returns [0, 9]</li>
     *      <li>{@link MatchInfo#getSnippet()} returns "Testing 1"</li>
     * </ul>
     */
    @SafeParcelable.Class(creator = "MatchInfoCreator")
    @SuppressWarnings("HiddenSuperclass")
    public static final class MatchInfo extends AbstractSafeParcelable {
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
        @NonNull public static final Parcelable.Creator<MatchInfo> CREATOR =
                new MatchInfoCreator();

        /** The path of the matching snippet property. */
        @Field(id = 1, getter = "getPropertyPath")
        private final String mPropertyPath;
        @Field(id = 2)
        final int mExactMatchRangeStart;
        @Field(id = 3)
        final int mExactMatchRangeEnd;
        @Field(id = 4)
        final int mSubmatchRangeStart;
        @Field(id = 5)
        final int mSubmatchRangeEnd;
        @Field(id = 6)
        final int mSnippetRangeStart;
        @Field(id = 7)
        final int mSnippetRangeEnd;

        @Nullable
        private PropertyPath mPropertyPathObject = null;

        /**
         * 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
        private GenericDocument mDocument = null;

        /** 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 mExactMatchRangeCached;

        /**
         * 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 mSubmatchRangeCached;

        /** Range of some reasonable amount of context around the query. Populated on first use. */
        @Nullable
        private MatchRange mWindowRangeCached;

        @Constructor
        MatchInfo(
                @Param(id = 1) @NonNull 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) {
            mPropertyPath = Preconditions.checkNotNull(propertyPath);
            mExactMatchRangeStart = exactMatchRangeStart;
            mExactMatchRangeEnd = exactMatchRangeEnd;
            mSubmatchRangeStart = submatchRangeStart;
            mSubmatchRangeEnd = submatchRangeEnd;
            mSnippetRangeStart = snippetRangeStart;
            mSnippetRangeEnd = snippetRangeEnd;
        }

        /**
         * 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 (mExactMatchRangeCached == null) {
                mExactMatchRangeCached = new MatchRange(
                        mExactMatchRangeStart,
                        mExactMatchRangeEnd);
            }
            return mExactMatchRangeCached;
        }

        /**
         * 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].
         *
         * <!--@exportToFramework:ifJetpack()-->
         * <p>This information may not be available depending on the backend and Android API
         * level. To ensure it is available, call {@link Features#isFeatureSupported}.
         *
         * @throws UnsupportedOperationException if {@link Features#isFeatureSupported} is
         * false.
         * <!--@exportToFramework:else()-->
         */
        @RequiresFeature(
                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
        @NonNull
        public MatchRange getSubmatchRange() {
            checkSubmatchSupported();
            if (mSubmatchRangeCached == null) {
                mSubmatchRangeCached = new MatchRange(
                        mSubmatchRangeStart,
                        mSubmatchRangeEnd);
            }
            return mSubmatchRangeCached;
        }

        /**
         * 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".
         *
         * <!--@exportToFramework:ifJetpack()-->
         * <p>This information may not be available depending on the backend and Android API
         * level. To ensure it is available, call {@link Features#isFeatureSupported}.
         *
         * @throws UnsupportedOperationException if {@link Features#isFeatureSupported} is
         * false.
         * <!--@exportToFramework:else()-->
         */
        @RequiresFeature(
                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
        @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 (mWindowRangeCached == null) {
                mWindowRangeCached = new MatchRange(
                        mSnippetRangeStart,
                        mSnippetRangeEnd);
            }
            return mWindowRangeCached;
        }

        /**
         * 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 (mSubmatchRangeStart == -1) {
                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;
        }

        /**
         * Sets the {@link GenericDocument} for {@link MatchInfo}.
         *
         * {@link MatchInfo} lacks a constructor that populates {@link MatchInfo#mDocument}
         * This provides the ability to set {@link MatchInfo#mDocument}
         */
        void setDocument(@NonNull GenericDocument document) {
            mDocument = document;
        }

        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            MatchInfoCreator.writeToParcel(this, dest, flags);
        }

        /** Builder for {@link MatchInfo} objects. */
        public static final class Builder {
            private final String mPropertyPath;
            private MatchRange mExactMatchRange = new MatchRange(0, 0);
            int mSubmatchRangeStart = -1;
            int mSubmatchRangeEnd = -1;
            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 = Preconditions.checkNotNull(propertyPath);
            }

            /** @exportToFramework:hide */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
            public Builder(@NonNull MatchInfo matchInfo) {
                Preconditions.checkNotNull(matchInfo);
                mPropertyPath = matchInfo.mPropertyPath;
                mExactMatchRange = matchInfo.getExactMatchRange();
                mSubmatchRangeStart = matchInfo.mSubmatchRangeStart;
                mSubmatchRangeEnd = matchInfo.mSubmatchRangeEnd;
                mSnippetRange = matchInfo.getSnippetRange();
            }

            /** Sets the exact {@link MatchRange} corresponding to the given entry. */
            @CanIgnoreReturnValue
            @NonNull
            public Builder setExactMatchRange(@NonNull MatchRange matchRange) {
                mExactMatchRange = Preconditions.checkNotNull(matchRange);
                return this;
            }


            /**
             * Sets the start and end of a submatch {@link MatchRange} corresponding
             * to the given entry.
             */
            @CanIgnoreReturnValue
            @NonNull
            public Builder setSubmatchRange(@NonNull MatchRange matchRange) {
                mSubmatchRangeStart = matchRange.getStart();
                mSubmatchRangeEnd = matchRange.getEnd();
                return this;
            }

            /** Sets the snippet {@link MatchRange} corresponding to the given entry. */
            @CanIgnoreReturnValue
            @NonNull
            public Builder setSnippetRange(@NonNull MatchRange matchRange) {
                mSnippetRange = Preconditions.checkNotNull(matchRange);
                return this;
            }

            /** Constructs a new {@link MatchInfo}. */
            @NonNull
            public MatchInfo build() {
                return new MatchInfo(
                    mPropertyPath,
                    mExactMatchRange.getStart(),
                    mExactMatchRange.getEnd(),
                    mSubmatchRangeStart,
                    mSubmatchRangeEnd,
                    mSnippetRange.getStart(),
                    mSnippetRange.getEnd());
            }
        }
    }

    /**
     * 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) represents 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 ObjectsCompat.hash(mStart, mEnd);
        }
    }
}