SearchResultsImpl.java

/*
 * 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.
 */
// @exportToFramework:skipFile()
package androidx.appsearch.localstorage;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResultPage;
import androidx.appsearch.app.SearchResults;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.localstorage.stats.SearchStats;
import androidx.appsearch.localstorage.util.FutureUtil;
import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
import androidx.core.util.Preconditions;

import com.google.common.util.concurrent.ListenableFuture;

import java.util.List;
import java.util.concurrent.Executor;

class SearchResultsImpl implements SearchResults {
    private final AppSearchImpl mAppSearchImpl;

    private final Executor mExecutor;

    /* The package name of the current app which is using the local backend. */
    private final String mPackageName;

    /** A CallerAccess object describing local-only access of the current app. */
    private final CallerAccess mSelfCallerAccess;

    /* The database name to search over. If null, this will search over all database names. */
    @Nullable
    private final String mDatabaseName;

    private final String mQueryExpression;

    private final SearchSpec mSearchSpec;

    private long mNextPageToken;

    private boolean mIsFirstLoad = true;

    private boolean mIsClosed = false;

    @Nullable
    private final AppSearchLogger mLogger;

    // Visibility Scope(local vs global) for 1st query, so it can be used for the visibility
    // scope for getNextPage().
    @SearchStats.VisibilityScope
    private int mVisibilityScope = SearchStats.VISIBILITY_SCOPE_UNKNOWN;

    SearchResultsImpl(
            @NonNull AppSearchImpl appSearchImpl,
            @NonNull Executor executor,
            @NonNull String packageName,
            @Nullable String databaseName,
            @NonNull String queryExpression,
            @NonNull SearchSpec searchSpec,
            @Nullable AppSearchLogger logger) {
        mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
        mExecutor = Preconditions.checkNotNull(executor);
        mPackageName = Preconditions.checkNotNull(packageName);
        mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mPackageName);
        mDatabaseName = databaseName;
        mQueryExpression = Preconditions.checkNotNull(queryExpression);
        mSearchSpec = Preconditions.checkNotNull(searchSpec);
        mLogger = logger;
    }

    @Override
    @NonNull
    public ListenableFuture<List<SearchResult>> getNextPageAsync() {
        Preconditions.checkState(!mIsClosed, "SearchResults has already been closed");
        return FutureUtil.execute(mExecutor, () -> {
            SearchResultPage searchResultPage;
            if (mIsFirstLoad) {
                mIsFirstLoad = false;
                if (mDatabaseName == null) {
                    mVisibilityScope = SearchStats.VISIBILITY_SCOPE_GLOBAL;
                    // Global queries aren't restricted to a single database
                    searchResultPage = mAppSearchImpl.globalQuery(
                            mQueryExpression, mSearchSpec, mSelfCallerAccess, mLogger);
                } else {
                    mVisibilityScope = SearchStats.VISIBILITY_SCOPE_LOCAL;
                    // Normal local query, pass in specified database.
                    searchResultPage = mAppSearchImpl.query(
                            mPackageName, mDatabaseName, mQueryExpression, mSearchSpec, mLogger);
                }
            } else {
                SearchStats.Builder sStatsBuilder = null;
                if (mLogger != null) {
                    sStatsBuilder =
                            new SearchStats.Builder(mVisibilityScope, mPackageName);
                    if (mDatabaseName != null) {
                        sStatsBuilder.setDatabase(mDatabaseName);
                    }
                }
                searchResultPage = mAppSearchImpl.getNextPage(mPackageName, mNextPageToken,
                        sStatsBuilder);
                if (mLogger != null && sStatsBuilder != null) {
                    mLogger.logStats(sStatsBuilder.build());
                }
            }
            mNextPageToken = searchResultPage.getNextPageToken();
            return searchResultPage.getResults();
        });
    }

    @Override
    @SuppressWarnings("FutureReturnValueIgnored")
    public void close() {
        // Checking the future result is not needed here since this is a cleanup step which is not
        // critical to the correct functioning of the system; also, the return value is void.
        if (!mIsClosed) {
            FutureUtil.execute(mExecutor, () -> {
                mAppSearchImpl.invalidateNextPageToken(mPackageName, mNextPageToken);
                mIsClosed = true;
                return null;
            });
        }
    }
}