1326 lines
69 KiB
Java
1326 lines
69 KiB
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.
|
|
*/
|
|
|
|
package android.app.appsearch;
|
|
|
|
import static android.app.appsearch.AppSearchResult.RESULT_INTERNAL_ERROR;
|
|
import static android.app.appsearch.SearchSessionUtil.safeExecute;
|
|
|
|
import android.annotation.CallbackExecutor;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.app.appsearch.aidl.AppSearchAttributionSource;
|
|
import android.app.appsearch.aidl.AppSearchBatchResultParcel;
|
|
import android.app.appsearch.aidl.AppSearchResultParcel;
|
|
import android.app.appsearch.aidl.DocumentsParcel;
|
|
import android.app.appsearch.aidl.GetDocumentsAidlRequest;
|
|
import android.app.appsearch.aidl.GetNamespacesAidlRequest;
|
|
import android.app.appsearch.aidl.GetSchemaAidlRequest;
|
|
import android.app.appsearch.aidl.GetStorageInfoAidlRequest;
|
|
import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
|
|
import android.app.appsearch.aidl.IAppSearchManager;
|
|
import android.app.appsearch.aidl.IAppSearchResultCallback;
|
|
import android.app.appsearch.aidl.InitializeAidlRequest;
|
|
import android.app.appsearch.aidl.PersistToDiskAidlRequest;
|
|
import android.app.appsearch.aidl.PutDocumentsAidlRequest;
|
|
import android.app.appsearch.aidl.RemoveByDocumentIdAidlRequest;
|
|
import android.app.appsearch.aidl.RemoveByQueryAidlRequest;
|
|
import android.app.appsearch.aidl.ReportUsageAidlRequest;
|
|
import android.app.appsearch.aidl.SearchSuggestionAidlRequest;
|
|
import android.app.appsearch.aidl.SetSchemaAidlRequest;
|
|
import android.app.appsearch.exceptions.AppSearchException;
|
|
import android.app.appsearch.safeparcel.GenericDocumentParcel;
|
|
import android.app.appsearch.stats.SchemaMigrationStats;
|
|
import android.app.appsearch.util.ExceptionUtil;
|
|
import android.app.appsearch.util.SchemaMigrationUtil;
|
|
import android.os.Build;
|
|
import android.os.RemoteException;
|
|
import android.os.SystemClock;
|
|
import android.os.UserHandle;
|
|
import android.util.ArraySet;
|
|
import android.util.Log;
|
|
|
|
import com.android.internal.util.Preconditions;
|
|
|
|
import java.io.Closeable;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* Provides a connection to a single AppSearch database.
|
|
*
|
|
* <p>An {@link AppSearchSession} instance provides access to database operations such as setting a
|
|
* schema, adding documents, and searching.
|
|
*
|
|
* <p>This class is thread safe.
|
|
*
|
|
* @see GlobalSearchSession
|
|
*/
|
|
public final class AppSearchSession implements Closeable {
|
|
private static final String TAG = "AppSearchSession";
|
|
|
|
private final AppSearchAttributionSource mCallerAttributionSource;
|
|
private final String mDatabaseName;
|
|
private final UserHandle mUserHandle;
|
|
private final IAppSearchManager mService;
|
|
@Nullable private final File mCacheDirectory;
|
|
|
|
private boolean mIsMutated = false;
|
|
private boolean mIsClosed = false;
|
|
|
|
/**
|
|
* Creates a search session for the client, defined by the {@code userHandle} and {@code
|
|
* packageName}.
|
|
*
|
|
* @param searchContext The {@link AppSearchManager.SearchContext} contains all information to
|
|
* create a new {@link AppSearchSession}.
|
|
* @param service The {@link IAppSearchManager} service from which to make api calls.
|
|
* @param userHandle The user for which the session should be created.
|
|
* @param callerAttributionSource The attribution source containing the caller's package name
|
|
* and uid.
|
|
* @param cacheDirectory The directory to create temporary files needed for migration. If this
|
|
* is null, the default temporary-file directory (/data/local/tmp) will be used.
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback The {@link AppSearchResult}<{@link AppSearchSession}> of performing
|
|
* this operation. Or a {@link AppSearchResult} with failure reason code and error
|
|
* information.
|
|
*/
|
|
static void createSearchSession(
|
|
@NonNull AppSearchManager.SearchContext searchContext,
|
|
@NonNull IAppSearchManager service,
|
|
@NonNull UserHandle userHandle,
|
|
@NonNull AppSearchAttributionSource callerAttributionSource,
|
|
@Nullable File cacheDirectory,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
|
|
AppSearchSession searchSession =
|
|
new AppSearchSession(
|
|
service,
|
|
userHandle,
|
|
callerAttributionSource,
|
|
searchContext.mDatabaseName,
|
|
cacheDirectory);
|
|
searchSession.initialize(executor, callback);
|
|
}
|
|
|
|
// NOTE: No instance of this class should be created or returned except via initialize().
|
|
// Once the callback.accept has been called here, the class is ready to use.
|
|
private void initialize(
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
|
|
try {
|
|
mService.initialize(
|
|
new InitializeAidlRequest(
|
|
mCallerAttributionSource,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> {
|
|
AppSearchResult<Void> result = resultParcel.getResult();
|
|
if (result.isSuccess()) {
|
|
callback.accept(
|
|
AppSearchResult.newSuccessfulResult(
|
|
AppSearchSession.this));
|
|
} else {
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(result));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
private AppSearchSession(
|
|
@NonNull IAppSearchManager service,
|
|
@NonNull UserHandle userHandle,
|
|
@NonNull AppSearchAttributionSource callerAttributionSource,
|
|
@NonNull String databaseName,
|
|
@Nullable File cacheDirectory) {
|
|
mService = service;
|
|
mUserHandle = userHandle;
|
|
mCallerAttributionSource = callerAttributionSource;
|
|
mDatabaseName = databaseName;
|
|
mCacheDirectory = cacheDirectory;
|
|
}
|
|
|
|
/**
|
|
* Sets the schema that represents the organizational structure of data within the AppSearch
|
|
* database.
|
|
*
|
|
* <p>Upon creating an {@link AppSearchSession}, {@link #setSchema} should be called. If the
|
|
* schema needs to be updated, or it has not been previously set, then the provided schema will
|
|
* be saved and persisted to disk. Otherwise, {@link #setSchema} is handled efficiently as a
|
|
* no-op call.
|
|
*
|
|
* @param request the schema to set or update the AppSearch database to.
|
|
* @param workExecutor Executor on which to schedule heavy client-side background work such as
|
|
* transforming documents.
|
|
* @param callbackExecutor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive errors resulting from setting the schema. If the
|
|
* operation succeeds, the callback will be invoked with {@code null}.
|
|
*/
|
|
public void setSchema(
|
|
@NonNull SetSchemaRequest request,
|
|
@NonNull Executor workExecutor,
|
|
@NonNull @CallbackExecutor Executor callbackExecutor,
|
|
@NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
|
|
Objects.requireNonNull(request);
|
|
Objects.requireNonNull(workExecutor);
|
|
Objects.requireNonNull(callbackExecutor);
|
|
Objects.requireNonNull(callback);
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
List<AppSearchSchema> schemaList = new ArrayList<>(request.getSchemas());
|
|
for (int i = 0; i < schemaList.size(); i++) {
|
|
if (!schemaList.get(i).getParentTypes().isEmpty()
|
|
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
throw new UnsupportedOperationException(
|
|
"SCHEMA_ADD_PARENT_TYPE is not available on this AppSearch "
|
|
+ "implementation.");
|
|
}
|
|
}
|
|
|
|
// Extract a List<VisibilityConfig> from the request
|
|
List<InternalVisibilityConfig> visibilityConfigs =
|
|
InternalVisibilityConfig.toInternalVisibilityConfigs(request);
|
|
// No need to trigger migration if user never set migrator
|
|
if (request.getMigrators().isEmpty()) {
|
|
setSchemaNoMigrations(
|
|
request, schemaList, visibilityConfigs, callbackExecutor, callback);
|
|
} else {
|
|
setSchemaWithMigrations(
|
|
request,
|
|
schemaList,
|
|
visibilityConfigs,
|
|
workExecutor,
|
|
callbackExecutor,
|
|
callback);
|
|
}
|
|
mIsMutated = true;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the schema most recently successfully provided to {@link #setSchema}.
|
|
*
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive the pending results of schema.
|
|
*/
|
|
public void getSchema(
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull Consumer<AppSearchResult<GetSchemaResponse>> callback) {
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(callback);
|
|
String targetPackageName = mCallerAttributionSource.getPackageName();
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
try {
|
|
mService.getSchema(
|
|
new GetSchemaAidlRequest(
|
|
mCallerAttributionSource,
|
|
targetPackageName,
|
|
mDatabaseName,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
|
|
/* isForEnterprise= */ false),
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> {
|
|
AppSearchResult<GetSchemaResponse> result =
|
|
resultParcel.getResult();
|
|
if (result.isSuccess()) {
|
|
GetSchemaResponse response =
|
|
Objects.requireNonNull(result.getResultValue());
|
|
callback.accept(
|
|
AppSearchResult.newSuccessfulResult(response));
|
|
} else {
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(result));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves the set of all namespaces in the current database with at least one document.
|
|
*
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive the namespaces.
|
|
*/
|
|
public void getNamespaces(
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull Consumer<AppSearchResult<Set<String>>> callback) {
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(callback);
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
try {
|
|
mService.getNamespaces(
|
|
new GetNamespacesAidlRequest(
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> {
|
|
AppSearchResult<List<String>> result =
|
|
resultParcel.getResult();
|
|
if (result.isSuccess()) {
|
|
Set<String> namespaces =
|
|
new ArraySet<>(result.getResultValue());
|
|
callback.accept(
|
|
AppSearchResult.newSuccessfulResult(
|
|
namespaces));
|
|
} else {
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(result));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indexes documents into the {@link AppSearchSession} database.
|
|
*
|
|
* <p>Each {@link GenericDocument} object must have a {@code schemaType} field set to an {@link
|
|
* AppSearchSchema} type that has been previously registered by calling the {@link #setSchema}
|
|
* method.
|
|
*
|
|
* @param request containing documents to be indexed.
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive pending result of performing this operation. The keys of
|
|
* the returned {@link AppSearchBatchResult} are the IDs of the input documents. The values
|
|
* are {@code null} if they were successfully indexed, or a failed {@link AppSearchResult}
|
|
* otherwise. If an unexpected internal error occurs in the AppSearch service, {@link
|
|
* BatchResultCallback#onSystemError} will be invoked with a {@link Throwable}.
|
|
*/
|
|
public void put(
|
|
@NonNull PutDocumentsRequest request,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull BatchResultCallback<String, Void> callback) {
|
|
Objects.requireNonNull(request);
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(callback);
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
DocumentsParcel documentsParcel =
|
|
new DocumentsParcel(
|
|
toGenericDocumentParcels(request.getGenericDocuments()),
|
|
toGenericDocumentParcels(request.getTakenActionGenericDocuments()));
|
|
try {
|
|
mService.putDocuments(
|
|
new PutDocumentsAidlRequest(
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
documentsParcel,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
|
|
new IAppSearchBatchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchBatchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> callback.onResult(resultParcel.getResult()));
|
|
}
|
|
|
|
@Override
|
|
public void onSystemError(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() ->
|
|
SearchSessionUtil.sendSystemErrorToCallback(
|
|
resultParcel.getResult(), callback));
|
|
}
|
|
});
|
|
mIsMutated = true;
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets {@link GenericDocument} objects by document IDs in a namespace from the {@link
|
|
* AppSearchSession} database.
|
|
*
|
|
* @param request a request containing a namespace and IDs to get documents for.
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive the pending result of performing this operation. The keys
|
|
* of the returned {@link AppSearchBatchResult} are the input IDs. The values are the
|
|
* returned {@link GenericDocument}s on success, or a failed {@link AppSearchResult}
|
|
* otherwise. IDs that are not found will return a failed {@link AppSearchResult} with a
|
|
* result code of {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
|
|
* occurs in the AppSearch service, {@link BatchResultCallback#onSystemError} will be
|
|
* invoked with a {@link Throwable}.
|
|
*/
|
|
public void getByDocumentId(
|
|
@NonNull GetByDocumentIdRequest request,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull BatchResultCallback<String, GenericDocument> callback) {
|
|
Objects.requireNonNull(request);
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(callback);
|
|
String targetPackageName = mCallerAttributionSource.getPackageName();
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
try {
|
|
mService.getDocuments(
|
|
new GetDocumentsAidlRequest(
|
|
mCallerAttributionSource,
|
|
targetPackageName,
|
|
mDatabaseName,
|
|
request,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
|
|
/* isForEnterprise= */ false),
|
|
SearchSessionUtil.createGetDocumentCallback(executor, callback));
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves documents from the open {@link AppSearchSession} that match a given query string
|
|
* and type of search provided.
|
|
*
|
|
* <p>Query strings can be empty, contain one term with no operators, or contain multiple terms
|
|
* and operators.
|
|
*
|
|
* <p>For query strings that are empty, all documents that match the {@link SearchSpec} will be
|
|
* returned.
|
|
*
|
|
* <p>For query strings with a single term and no operators, documents that match the provided
|
|
* query string and {@link SearchSpec} will be returned.
|
|
*
|
|
* <p>The following operators are supported:
|
|
*
|
|
* <ul>
|
|
* <li>AND (implicit)
|
|
* <p>AND is an operator that matches documents that contain <i>all</i> provided terms.
|
|
* <p><b>NOTE:</b> A space between terms is treated as an "AND" operator. Explicitly
|
|
* including "AND" in a query string will treat "AND" as a term, returning documents that
|
|
* also contain "AND".
|
|
* <p>Example: "apple AND banana" matches documents that contain the terms "apple", "and",
|
|
* "banana".
|
|
* <p>Example: "apple banana" matches documents that contain both "apple" and "banana".
|
|
* <p>Example: "apple banana cherry" matches documents that contain "apple", "banana", and
|
|
* "cherry".
|
|
* <li>OR
|
|
* <p>OR is an operator that matches documents that contain <i>any</i> provided term.
|
|
* <p>Example: "apple OR banana" matches documents that contain either "apple" or
|
|
* "banana".
|
|
* <p>Example: "apple OR banana OR cherry" matches documents that contain any of "apple",
|
|
* "banana", or "cherry".
|
|
* <li>Exclusion (-)
|
|
* <p>Exclusion (-) is an operator that matches documents that <i>do not</i> contain the
|
|
* provided term.
|
|
* <p>Example: "-apple" matches documents that do not contain "apple".
|
|
* <li>Grouped Terms
|
|
* <p>For queries that require multiple operators and terms, terms can be grouped into
|
|
* subqueries. Subqueries are contained within an open "(" and close ")" parenthesis.
|
|
* <p>Example: "(donut OR bagel) (coffee OR tea)" matches documents that contain either
|
|
* "donut" or "bagel" and either "coffee" or "tea".
|
|
* <li>Property Restricts
|
|
* <p>For queries that require a term to match a specific {@link AppSearchSchema} property
|
|
* of a document, a ":" must be included between the property name and the term.
|
|
* <p>Example: "subject:important" matches documents that contain the term "important" in
|
|
* the "subject" property.
|
|
* </ul>
|
|
*
|
|
* <p>The above description covers the basic query operators. Additional advanced query operator
|
|
* features should be explicitly enabled in the SearchSpec and are described below.
|
|
*
|
|
* <p>LIST_FILTER_QUERY_LANGUAGE: This feature covers the expansion of the query language to
|
|
* conform to the definition of the list filters language (https://aip.dev/160). This includes:
|
|
*
|
|
* <ul>
|
|
* <li>addition of explicit 'AND' and 'NOT' operators
|
|
* <li>property restricts are allowed with groupings (ex. "prop:(a OR b)")
|
|
* <li>addition of custom functions to control matching
|
|
* </ul>
|
|
*
|
|
* <p>The newly added custom functions covered by this feature are:
|
|
*
|
|
* <ul>
|
|
* <li>createList(String...)
|
|
* <li>search(String, List<String>)
|
|
* <li>propertyDefined(String)
|
|
* </ul>
|
|
*
|
|
* <p>createList takes a variable number of strings and returns a list of strings. It is for use
|
|
* with search.
|
|
*
|
|
* <p>search takes a query string that will be parsed according to the supported query language
|
|
* and an optional list of strings that specify the properties to be restricted to. This exists
|
|
* as a convenience for multiple property restricts. So, for example, the query `(subject:foo OR
|
|
* body:foo) (subject:bar OR body:bar)` could be rewritten as `search("foo bar",
|
|
* createList("subject", "bar"))`.
|
|
*
|
|
* <p>propertyDefined takes a string specifying the property of interest and matches all
|
|
* documents of any type that defines the specified property (ex.
|
|
* `propertyDefined("sender.name")`). Note that propertyDefined will match so long as the
|
|
* document's type defines the specified property. It does NOT require that the document
|
|
* actually hold any values for this property.
|
|
*
|
|
* <p>NUMERIC_SEARCH: This feature covers numeric search expressions. In the query language, the
|
|
* values of properties that have {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}
|
|
* set can be matched with a numeric search expression (the property, a supported comparator and
|
|
* an integer value). Supported comparators are <, <=, ==, >= and >.
|
|
*
|
|
* <p>Ex. `price < 10` will match all documents that has a numeric value in its price property
|
|
* that is less than 10.
|
|
*
|
|
* <p>VERBATIM_SEARCH: This feature covers the verbatim string operator (quotation marks).
|
|
*
|
|
* <p>Ex. `"foo/bar" OR baz` will ensure that 'foo/bar' is treated as a single 'verbatim' token.
|
|
*
|
|
* <p>Additional search specifications, such as filtering by {@link AppSearchSchema} type or
|
|
* adding projection, can be set by calling the corresponding {@link SearchSpec.Builder} setter.
|
|
*
|
|
* <p>This method is lightweight. The heavy work will be done in {@link
|
|
* SearchResults#getNextPage}.
|
|
*
|
|
* @param queryExpression query string to search.
|
|
* @param searchSpec spec for setting document filters, adding projection, setting term match
|
|
* type, etc.
|
|
* @return a {@link SearchResults} object for retrieved matched documents.
|
|
*/
|
|
@NonNull
|
|
public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
|
|
Objects.requireNonNull(queryExpression);
|
|
Objects.requireNonNull(searchSpec);
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
return new SearchResults(
|
|
mService,
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
queryExpression,
|
|
searchSpec,
|
|
mUserHandle,
|
|
/* isForEnterprise= */ false);
|
|
}
|
|
|
|
/**
|
|
* Retrieves suggested Strings that could be used as {@code queryExpression} in {@link
|
|
* #search(String, SearchSpec)} API.
|
|
*
|
|
* <p>The {@code suggestionQueryExpression} can contain one term with no operators, or contain
|
|
* multiple terms and operators. Operators will be considered as a normal term. Please see the
|
|
* operator examples below. The {@code suggestionQueryExpression} must end with a valid term,
|
|
* the suggestions are generated based on the last term. If the input {@code
|
|
* suggestionQueryExpression} doesn't have a valid token, AppSearch will return an empty result
|
|
* list. Please see the invalid examples below.
|
|
*
|
|
* <p>Example: if there are following documents with content stored in AppSearch.
|
|
*
|
|
* <ul>
|
|
* <li>document1: "term1"
|
|
* <li>document2: "term1 term2"
|
|
* <li>document3: "term1 term2 term3"
|
|
* <li>document4: "org"
|
|
* </ul>
|
|
*
|
|
* <p>Search suggestions with the single term {@code suggestionQueryExpression} "t", the
|
|
* suggested results are:
|
|
*
|
|
* <ul>
|
|
* <li>"term1" - Use it to be queryExpression in {@link #search} could get 3 {@link
|
|
* SearchResult}s, which contains document 1, 2 and 3.
|
|
* <li>"term2" - Use it to be queryExpression in {@link #search} could get 2 {@link
|
|
* SearchResult}s, which contains document 2 and 3.
|
|
* <li>"term3" - Use it to be queryExpression in {@link #search} could get 1 {@link
|
|
* SearchResult}, which contains document 3.
|
|
* </ul>
|
|
*
|
|
* <p>Search suggestions with the multiple term {@code suggestionQueryExpression} "org t", the
|
|
* suggested result will be "org term1" - The last token is completed by the suggested String.
|
|
*
|
|
* <p>Operators in {@link #search} are supported.
|
|
*
|
|
* <p><b>NOTE:</b> Exclusion and Grouped Terms in the last term is not supported.
|
|
*
|
|
* <p>example: "apple -f": This Api will throw an {@link
|
|
* android.app.appsearch.exceptions.AppSearchException} with {@link
|
|
* AppSearchResult#RESULT_INVALID_ARGUMENT}.
|
|
*
|
|
* <p>example: "apple (f)": This Api will return an empty results.
|
|
*
|
|
* <p>Invalid example: All these input {@code suggestionQueryExpression} don't have a valid last
|
|
* token, AppSearch will return an empty result list.
|
|
*
|
|
* <ul>
|
|
* <li>"" - Empty {@code suggestionQueryExpression}.
|
|
* <li>"(f)" - Ending in a closed brackets.
|
|
* <li>"f:" - Ending in an operator.
|
|
* <li>"f " - Ending in trailing space.
|
|
* </ul>
|
|
*
|
|
* @param suggestionQueryExpression the non empty query string to search suggestions
|
|
* @param searchSuggestionSpec spec for setting document filters
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive the pending result of performing this operation, which is
|
|
* a List of {@link SearchSuggestionResult} on success. The returned suggestion Strings are
|
|
* ordered by the number of {@link SearchResult} you could get by using that suggestion in
|
|
* {@link #search}.
|
|
* @see #search(String, SearchSpec)
|
|
*/
|
|
public void searchSuggestion(
|
|
@NonNull String suggestionQueryExpression,
|
|
@NonNull SearchSuggestionSpec searchSuggestionSpec,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull Consumer<AppSearchResult<List<SearchSuggestionResult>>> callback) {
|
|
Objects.requireNonNull(suggestionQueryExpression);
|
|
Objects.requireNonNull(searchSuggestionSpec);
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(callback);
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
try {
|
|
mService.searchSuggestion(
|
|
new SearchSuggestionAidlRequest(
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
suggestionQueryExpression,
|
|
searchSuggestionSpec,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> {
|
|
try {
|
|
AppSearchResult<List<SearchSuggestionResult>> result =
|
|
resultParcel.getResult();
|
|
if (result.isSuccess()) {
|
|
callback.accept(result);
|
|
} else {
|
|
// TODO(b/261897334) save SDK errors/crashes and
|
|
// send to
|
|
// server for logging.
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(result));
|
|
}
|
|
} catch (Exception e) {
|
|
callback.accept(
|
|
AppSearchResult.throwableToFailedResult(e));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reports usage of a particular document by namespace and ID.
|
|
*
|
|
* <p>A usage report represents an event in which a user interacted with or viewed a document.
|
|
*
|
|
* <p>For each call to {@link #reportUsage}, AppSearch updates usage count and usage recency
|
|
* metrics for that particular document. These metrics are used for ordering {@link #search}
|
|
* results by the {@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} and {@link
|
|
* SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} ranking strategies.
|
|
*
|
|
* <p>Reporting usage of a document is optional.
|
|
*
|
|
* @param request The usage reporting request.
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive errors. If the operation succeeds, the callback will be
|
|
* invoked with {@code null}.
|
|
*/
|
|
public void reportUsage(
|
|
@NonNull ReportUsageRequest request,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull Consumer<AppSearchResult<Void>> callback) {
|
|
Objects.requireNonNull(request);
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(callback);
|
|
String targetPackageName = mCallerAttributionSource.getPackageName();
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
try {
|
|
mService.reportUsage(
|
|
new ReportUsageAidlRequest(
|
|
mCallerAttributionSource,
|
|
targetPackageName,
|
|
mDatabaseName,
|
|
request,
|
|
/* systemUsage= */ false,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> callback.accept(resultParcel.getResult()));
|
|
}
|
|
});
|
|
mIsMutated = true;
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes {@link GenericDocument} objects by document IDs in a namespace from the {@link
|
|
* AppSearchSession} database.
|
|
*
|
|
* <p>Removed documents will no longer be surfaced by {@link #search} or {@link
|
|
* #getByDocumentId} calls.
|
|
*
|
|
* <p>Once the database crosses the document count or byte usage threshold, removed documents
|
|
* will be deleted from disk.
|
|
*
|
|
* @param request {@link RemoveByDocumentIdRequest} with IDs in a namespace to remove from the
|
|
* index.
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive the pending result of performing this operation. The keys
|
|
* of the returned {@link AppSearchBatchResult} are the input document IDs. The values are
|
|
* {@code null} on success, or a failed {@link AppSearchResult} otherwise. IDs that are not
|
|
* found will return a failed {@link AppSearchResult} with a result code of {@link
|
|
* AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error occurs in the
|
|
* AppSearch service, {@link BatchResultCallback#onSystemError} will be invoked with a
|
|
* {@link Throwable}.
|
|
*/
|
|
public void remove(
|
|
@NonNull RemoveByDocumentIdRequest request,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull BatchResultCallback<String, Void> callback) {
|
|
Objects.requireNonNull(request);
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(callback);
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
try {
|
|
mService.removeByDocumentId(
|
|
new RemoveByDocumentIdAidlRequest(
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
request,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
|
|
new IAppSearchBatchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchBatchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> callback.onResult(resultParcel.getResult()));
|
|
}
|
|
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onSystemError(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() ->
|
|
SearchSessionUtil.sendSystemErrorToCallback(
|
|
resultParcel.getResult(), callback));
|
|
}
|
|
});
|
|
mIsMutated = true;
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
|
|
* match the {@code queryExpression} in given namespaces and schemaTypes which is set via {@link
|
|
* SearchSpec.Builder#addFilterNamespaces} and {@link SearchSpec.Builder#addFilterSchemas}.
|
|
*
|
|
* <p>An empty {@code queryExpression} matches all documents.
|
|
*
|
|
* <p>An empty set of namespaces or schemaTypes matches all namespaces or schemaTypes in the
|
|
* current database.
|
|
*
|
|
* @param queryExpression Query String to search.
|
|
* @param searchSpec Spec containing schemaTypes, namespaces and query expression indicates how
|
|
* document will be removed. All specific about how to scoring, ordering, snippeting and
|
|
* resulting will be ignored.
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive errors resulting from removing the documents. If the
|
|
* operation succeeds, the callback will be invoked with {@code null}.
|
|
*/
|
|
public void remove(
|
|
@NonNull String queryExpression,
|
|
@NonNull SearchSpec searchSpec,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull Consumer<AppSearchResult<Void>> callback) {
|
|
Objects.requireNonNull(queryExpression);
|
|
Objects.requireNonNull(searchSpec);
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(callback);
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
if (searchSpec.getJoinSpec() != null) {
|
|
throw new IllegalArgumentException(
|
|
"JoinSpec not allowed in removeByQuery, but " + "JoinSpec was provided.");
|
|
}
|
|
try {
|
|
mService.removeByQuery(
|
|
new RemoveByQueryAidlRequest(
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
queryExpression,
|
|
searchSpec,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> callback.accept(resultParcel.getResult()));
|
|
}
|
|
});
|
|
mIsMutated = true;
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the storage info for this {@link AppSearchSession} database.
|
|
*
|
|
* <p>This may take time proportional to the number of documents and may be inefficient to call
|
|
* repeatedly.
|
|
*
|
|
* @param executor Executor on which to invoke the callback.
|
|
* @param callback Callback to receive the storage info.
|
|
*/
|
|
public void getStorageInfo(
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull Consumer<AppSearchResult<StorageInfo>> callback) {
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(callback);
|
|
Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
|
|
try {
|
|
mService.getStorageInfo(
|
|
new GetStorageInfoAidlRequest(
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> {
|
|
AppSearchResult<StorageInfo> result =
|
|
resultParcel.getResult();
|
|
if (result.isSuccess()) {
|
|
callback.accept(
|
|
AppSearchResult.newSuccessfulResult(
|
|
result.getResultValue()));
|
|
} else {
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(result));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes the {@link AppSearchSession} to persist all schema and document updates, additions,
|
|
* and deletes to disk.
|
|
*/
|
|
@Override
|
|
public void close() {
|
|
if (mIsMutated && !mIsClosed) {
|
|
try {
|
|
mService.persistToDisk(
|
|
new PersistToDiskAidlRequest(
|
|
mCallerAttributionSource,
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()));
|
|
mIsClosed = true;
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Unable to close the AppSearchSession", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set schema to Icing for no-migration scenario.
|
|
*
|
|
* <p>We only need one time {@link #setSchema} call for no-migration scenario by using the
|
|
* forceoverride in the request.
|
|
*/
|
|
private void setSchemaNoMigrations(
|
|
@NonNull SetSchemaRequest request,
|
|
@NonNull List<AppSearchSchema> schemas,
|
|
@NonNull List<InternalVisibilityConfig> visibilityConfigs,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
|
|
try {
|
|
SetSchemaAidlRequest setSchemaAidlRequest =
|
|
new SetSchemaAidlRequest(
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
schemas,
|
|
visibilityConfigs,
|
|
request.isForceOverride(),
|
|
request.getVersion(),
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
|
|
SchemaMigrationStats.NO_MIGRATION);
|
|
mService.setSchema(
|
|
setSchemaAidlRequest,
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchResultParcel resultParcel) {
|
|
safeExecute(
|
|
executor,
|
|
callback,
|
|
() -> {
|
|
AppSearchResult<InternalSetSchemaResponse> result =
|
|
resultParcel.getResult();
|
|
if (result.isSuccess()) {
|
|
try {
|
|
InternalSetSchemaResponse
|
|
internalSetSchemaResponse =
|
|
result.getResultValue();
|
|
if (internalSetSchemaResponse == null) {
|
|
// Ideally internalSetSchemaResponse should
|
|
// always be non-null as result is success. In
|
|
// other cases we directly put result in
|
|
// AppSearchResult.newSuccessfulResult which
|
|
// accepts a Nullable value, here we need to
|
|
// get response by
|
|
// internalSetSchemaResponse
|
|
// .getSetSchemaResponse().
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(
|
|
RESULT_INTERNAL_ERROR,
|
|
"Received null"
|
|
+ " InternalSetSchema"
|
|
+ "Response"
|
|
+ " during setSchema"
|
|
+ " call"));
|
|
return;
|
|
}
|
|
if (!internalSetSchemaResponse.isSuccess()) {
|
|
// check is the set schema call failed
|
|
// because incompatible changes. That's the only
|
|
// case we swallowed in the
|
|
// AppSearchImpl#setSchema().
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(
|
|
AppSearchResult
|
|
.RESULT_INVALID_SCHEMA,
|
|
internalSetSchemaResponse
|
|
.getErrorMessage()));
|
|
return;
|
|
}
|
|
callback.accept(
|
|
AppSearchResult.newSuccessfulResult(
|
|
internalSetSchemaResponse
|
|
.getSetSchemaResponse()));
|
|
} catch (RuntimeException e) {
|
|
// TODO(b/261897334) save SDK errors/crashes and
|
|
// send to
|
|
// server for logging.
|
|
callback.accept(
|
|
AppSearchResult.throwableToFailedResult(e));
|
|
}
|
|
} else {
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(result));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (RemoteException e) {
|
|
ExceptionUtil.handleRemoteException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set schema to Icing for migration scenario.
|
|
*
|
|
* <p>First time {@link #setSchema} call with forceOverride is false gives us all incompatible
|
|
* changes. After trigger migrations, the second time call {@link #setSchema} will actually
|
|
* apply the changes.
|
|
*/
|
|
private void setSchemaWithMigrations(
|
|
@NonNull SetSchemaRequest request,
|
|
@NonNull List<AppSearchSchema> schemas,
|
|
@NonNull List<InternalVisibilityConfig> visibilityConfigs,
|
|
@NonNull Executor workExecutor,
|
|
@NonNull @CallbackExecutor Executor callbackExecutor,
|
|
@NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
|
|
long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
|
|
long waitExecutorStartLatencyMillis = SystemClock.elapsedRealtime();
|
|
safeExecute(
|
|
workExecutor,
|
|
callback,
|
|
() -> {
|
|
try {
|
|
long waitExecutorEndLatencyMillis = SystemClock.elapsedRealtime();
|
|
String packageName = mCallerAttributionSource.getPackageName();
|
|
SchemaMigrationStats.Builder statsBuilder =
|
|
new SchemaMigrationStats.Builder(packageName, mDatabaseName);
|
|
|
|
// Migration process
|
|
// 1. Validate and retrieve all active migrators.
|
|
long getSchemaLatencyStartTimeMillis = SystemClock.elapsedRealtime();
|
|
CountDownLatch getSchemaLatch = new CountDownLatch(1);
|
|
AtomicReference<AppSearchResult<GetSchemaResponse>> getSchemaResultRef =
|
|
new AtomicReference<>();
|
|
getSchema(
|
|
callbackExecutor,
|
|
(result) -> {
|
|
getSchemaResultRef.set(result);
|
|
getSchemaLatch.countDown();
|
|
});
|
|
getSchemaLatch.await();
|
|
AppSearchResult<GetSchemaResponse> getSchemaResult =
|
|
getSchemaResultRef.get();
|
|
if (!getSchemaResult.isSuccess()) {
|
|
// TODO(b/261897334) save SDK errors/crashes and send to server for
|
|
// logging.
|
|
safeExecute(
|
|
callbackExecutor,
|
|
callback,
|
|
() ->
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(
|
|
getSchemaResult)));
|
|
return;
|
|
}
|
|
GetSchemaResponse getSchemaResponse =
|
|
Objects.requireNonNull(getSchemaResult.getResultValue());
|
|
int currentVersion = getSchemaResponse.getVersion();
|
|
int finalVersion = request.getVersion();
|
|
Map<String, Migrator> activeMigrators =
|
|
SchemaMigrationUtil.getActiveMigrators(
|
|
getSchemaResponse.getSchemas(),
|
|
request.getMigrators(),
|
|
currentVersion,
|
|
finalVersion);
|
|
long getSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
|
|
|
|
// No need to trigger migration if no migrator is active.
|
|
if (activeMigrators.isEmpty()) {
|
|
setSchemaNoMigrations(
|
|
request,
|
|
schemas,
|
|
visibilityConfigs,
|
|
callbackExecutor,
|
|
callback);
|
|
return;
|
|
}
|
|
|
|
// 2. SetSchema with forceOverride=false, to retrieve the list of
|
|
// incompatible/deleted types.
|
|
long firstSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
|
|
CountDownLatch setSchemaLatch = new CountDownLatch(1);
|
|
AtomicReference<AppSearchResult<InternalSetSchemaResponse>>
|
|
setSchemaResultRef = new AtomicReference<>();
|
|
|
|
SetSchemaAidlRequest setSchemaAidlRequest =
|
|
new SetSchemaAidlRequest(
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
schemas,
|
|
visibilityConfigs,
|
|
/* forceOverride= */ false,
|
|
request.getVersion(),
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock
|
|
.elapsedRealtime(),
|
|
SchemaMigrationStats.FIRST_CALL_GET_INCOMPATIBLE);
|
|
mService.setSchema(
|
|
setSchemaAidlRequest,
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(AppSearchResultParcel resultParcel) {
|
|
setSchemaResultRef.set(resultParcel.getResult());
|
|
setSchemaLatch.countDown();
|
|
}
|
|
});
|
|
setSchemaLatch.await();
|
|
AppSearchResult<InternalSetSchemaResponse> setSchemaResult =
|
|
setSchemaResultRef.get();
|
|
if (!setSchemaResult.isSuccess()) {
|
|
// TODO(b/261897334) save SDK errors/crashes and send to server for
|
|
// logging.
|
|
safeExecute(
|
|
callbackExecutor,
|
|
callback,
|
|
() ->
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(
|
|
setSchemaResult)));
|
|
return;
|
|
}
|
|
InternalSetSchemaResponse internalSetSchemaResponse1 =
|
|
setSchemaResult.getResultValue();
|
|
if (internalSetSchemaResponse1 == null) {
|
|
safeExecute(
|
|
callbackExecutor,
|
|
callback,
|
|
() ->
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(
|
|
RESULT_INTERNAL_ERROR,
|
|
"Received null"
|
|
+ " InternalSetSchemaResponse"
|
|
+ " during setSchema call")));
|
|
return;
|
|
}
|
|
long firstSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
|
|
|
|
// 3. If forceOverride is false, check that all incompatible types will be
|
|
// migrated.
|
|
// If some aren't we must throw an error, rather than proceeding and
|
|
// deleting those
|
|
// types.
|
|
SchemaMigrationUtil.checkDeletedAndIncompatibleAfterMigration(
|
|
internalSetSchemaResponse1, activeMigrators.keySet());
|
|
|
|
try (AppSearchMigrationHelper migrationHelper =
|
|
new AppSearchMigrationHelper(
|
|
mService,
|
|
mUserHandle,
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
request.getSchemas(),
|
|
mCacheDirectory)) {
|
|
|
|
// 4. Trigger migration for all migrators.
|
|
long queryAndTransformLatencyStartMillis =
|
|
SystemClock.elapsedRealtime();
|
|
for (Map.Entry<String, Migrator> entry : activeMigrators.entrySet()) {
|
|
migrationHelper.queryAndTransform(
|
|
/* schemaType= */ entry.getKey(),
|
|
/* migrator= */ entry.getValue(),
|
|
currentVersion,
|
|
finalVersion,
|
|
statsBuilder);
|
|
}
|
|
long queryAndTransformLatencyEndTimeMillis =
|
|
SystemClock.elapsedRealtime();
|
|
|
|
// 5. SetSchema a second time with forceOverride=true if the first
|
|
// attempted
|
|
// failed.
|
|
long secondSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
|
|
InternalSetSchemaResponse internalSetSchemaResponse;
|
|
if (internalSetSchemaResponse1.isSuccess()) {
|
|
internalSetSchemaResponse = internalSetSchemaResponse1;
|
|
} else {
|
|
CountDownLatch setSchema2Latch = new CountDownLatch(1);
|
|
AtomicReference<AppSearchResult<InternalSetSchemaResponse>>
|
|
setSchema2ResultRef = new AtomicReference<>();
|
|
// only trigger second setSchema() call if the first one is fail.
|
|
SetSchemaAidlRequest setSchemaAidlRequest1 =
|
|
new SetSchemaAidlRequest(
|
|
mCallerAttributionSource,
|
|
mDatabaseName,
|
|
schemas,
|
|
visibilityConfigs,
|
|
/* forceOverride= */ true,
|
|
request.getVersion(),
|
|
mUserHandle,
|
|
/* binderCallStartTimeMillis= */ SystemClock
|
|
.elapsedRealtime(),
|
|
SchemaMigrationStats.SECOND_CALL_APPLY_NEW_SCHEMA);
|
|
mService.setSchema(
|
|
setSchemaAidlRequest1,
|
|
new IAppSearchResultCallback.Stub() {
|
|
@Override
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
public void onResult(
|
|
AppSearchResultParcel resultParcel) {
|
|
setSchema2ResultRef.set(resultParcel.getResult());
|
|
setSchema2Latch.countDown();
|
|
}
|
|
});
|
|
setSchema2Latch.await();
|
|
AppSearchResult<InternalSetSchemaResponse> setSchema2Result =
|
|
setSchema2ResultRef.get();
|
|
if (!setSchema2Result.isSuccess()) {
|
|
// we failed to set the schema in second time with forceOverride
|
|
// = true, which is an impossible case. Since we only swallow
|
|
// the incompatible error in the first setSchema call, all other
|
|
// errors will be thrown at the first time.
|
|
// TODO(b/261897334) save SDK errors/crashes and send to server
|
|
// for logging.
|
|
safeExecute(
|
|
callbackExecutor,
|
|
callback,
|
|
() ->
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(
|
|
setSchema2Result)));
|
|
return;
|
|
}
|
|
InternalSetSchemaResponse internalSetSchemaResponse2 =
|
|
setSchema2Result.getResultValue();
|
|
if (internalSetSchemaResponse2 == null) {
|
|
safeExecute(
|
|
callbackExecutor,
|
|
callback,
|
|
() ->
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(
|
|
RESULT_INTERNAL_ERROR,
|
|
"Received null response"
|
|
+ " during setSchema"
|
|
+ " call")));
|
|
return;
|
|
}
|
|
if (!internalSetSchemaResponse2.isSuccess()) {
|
|
// Impossible case, we just set forceOverride to be true, we
|
|
// should never fail in incompatible changes. And all other
|
|
// cases should failed during the first call.
|
|
// TODO(b/261897334) save SDK errors/crashes and send to server
|
|
// for logging.
|
|
safeExecute(
|
|
callbackExecutor,
|
|
callback,
|
|
() ->
|
|
callback.accept(
|
|
AppSearchResult.newFailedResult(
|
|
RESULT_INTERNAL_ERROR,
|
|
internalSetSchemaResponse2
|
|
.getErrorMessage())));
|
|
return;
|
|
}
|
|
internalSetSchemaResponse = internalSetSchemaResponse2;
|
|
}
|
|
long secondSetSchemaLatencyEndTimeMillis =
|
|
SystemClock.elapsedRealtime();
|
|
|
|
statsBuilder
|
|
.setExecutorAcquisitionLatencyMillis(
|
|
(int)
|
|
(waitExecutorEndLatencyMillis
|
|
- waitExecutorStartLatencyMillis))
|
|
.setGetSchemaLatencyMillis(
|
|
(int)
|
|
(getSchemaLatencyEndTimeMillis
|
|
- getSchemaLatencyStartTimeMillis))
|
|
.setFirstSetSchemaLatencyMillis(
|
|
(int)
|
|
(firstSetSchemaLatencyEndTimeMillis
|
|
- firstSetSchemaLatencyStartMillis))
|
|
.setIsFirstSetSchemaSuccess(
|
|
internalSetSchemaResponse1.isSuccess())
|
|
.setQueryAndTransformLatencyMillis(
|
|
(int)
|
|
(queryAndTransformLatencyEndTimeMillis
|
|
- queryAndTransformLatencyStartMillis))
|
|
.setSecondSetSchemaLatencyMillis(
|
|
(int)
|
|
(secondSetSchemaLatencyEndTimeMillis
|
|
- secondSetSchemaLatencyStartMillis));
|
|
SetSchemaResponse.Builder responseBuilder =
|
|
new SetSchemaResponse.Builder(
|
|
internalSetSchemaResponse
|
|
.getSetSchemaResponse())
|
|
.addMigratedTypes(activeMigrators.keySet());
|
|
|
|
// 6. Put all the migrated documents into the index, now that the new
|
|
// schema is
|
|
// set.
|
|
AppSearchResult<SetSchemaResponse> putResult =
|
|
migrationHelper.putMigratedDocuments(
|
|
responseBuilder,
|
|
statsBuilder,
|
|
totalLatencyStartTimeMillis);
|
|
safeExecute(
|
|
callbackExecutor, callback, () -> callback.accept(putResult));
|
|
}
|
|
} catch (RemoteException
|
|
| AppSearchException
|
|
| InterruptedException
|
|
| IOException
|
|
| ExecutionException
|
|
| RuntimeException e) {
|
|
// TODO(b/261897334) save SDK errors/crashes and send to server for logging.
|
|
safeExecute(
|
|
callbackExecutor,
|
|
callback,
|
|
() -> callback.accept(AppSearchResult.throwableToFailedResult(e)));
|
|
}
|
|
});
|
|
}
|
|
|
|
@NonNull
|
|
private static List<GenericDocumentParcel> toGenericDocumentParcels(
|
|
List<GenericDocument> docs) {
|
|
List<GenericDocumentParcel> docParcels = new ArrayList<>(docs.size());
|
|
for (int i = 0; i < docs.size(); ++i) {
|
|
docParcels.add(docs.get(i).getDocumentParcel());
|
|
}
|
|
return docParcels;
|
|
}
|
|
}
|