• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app.appsearch;
18 
19 import static android.app.appsearch.SearchSessionUtil.safeExecute;
20 
21 import android.annotation.CallbackExecutor;
22 import android.annotation.NonNull;
23 import android.app.appsearch.aidl.AppSearchBatchResultParcel;
24 import android.app.appsearch.aidl.AppSearchResultParcel;
25 import android.app.appsearch.aidl.DocumentsParcel;
26 import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
27 import android.app.appsearch.aidl.IAppSearchManager;
28 import android.app.appsearch.aidl.IAppSearchResultCallback;
29 import android.app.appsearch.stats.SchemaMigrationStats;
30 import android.app.appsearch.util.SchemaMigrationUtil;
31 import android.content.AttributionSource;
32 import android.os.Bundle;
33 import android.os.RemoteException;
34 import android.os.SystemClock;
35 import android.os.UserHandle;
36 import android.util.ArraySet;
37 import android.util.Log;
38 
39 import com.android.internal.util.Preconditions;
40 
41 import java.io.Closeable;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Objects;
46 import java.util.Set;
47 import java.util.concurrent.CountDownLatch;
48 import java.util.concurrent.Executor;
49 import java.util.concurrent.atomic.AtomicReference;
50 import java.util.function.Consumer;
51 
52 /**
53  * Provides a connection to a single AppSearch database.
54  *
55  * <p>An {@link AppSearchSession} instance provides access to database operations such as
56  * setting a schema, adding documents, and searching.
57  *
58  * <p>This class is thread safe.
59  *
60  * @see GlobalSearchSession
61  */
62 public final class AppSearchSession implements Closeable {
63     private static final String TAG = "AppSearchSession";
64 
65     private final AttributionSource mCallerAttributionSource;
66     private final String mDatabaseName;
67     private final UserHandle mUserHandle;
68     private final IAppSearchManager mService;
69 
70     private boolean mIsMutated = false;
71     private boolean mIsClosed = false;
72 
73     /**
74      * Creates a search session for the client, defined by the {@code userHandle} and
75      * {@code packageName}.
76      */
createSearchSession( @onNull AppSearchManager.SearchContext searchContext, @NonNull IAppSearchManager service, @NonNull UserHandle userHandle, @NonNull AttributionSource callerAttributionSource, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<AppSearchSession>> callback)77     static void createSearchSession(
78             @NonNull AppSearchManager.SearchContext searchContext,
79             @NonNull IAppSearchManager service,
80             @NonNull UserHandle userHandle,
81             @NonNull AttributionSource callerAttributionSource,
82             @NonNull @CallbackExecutor Executor executor,
83             @NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
84         AppSearchSession searchSession =
85                 new AppSearchSession(service, userHandle, callerAttributionSource,
86                         searchContext.mDatabaseName);
87         searchSession.initialize(executor, callback);
88     }
89 
90     // NOTE: No instance of this class should be created or returned except via initialize().
91     // Once the callback.accept has been called here, the class is ready to use.
initialize( @onNull @allbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<AppSearchSession>> callback)92     private void initialize(
93             @NonNull @CallbackExecutor Executor executor,
94             @NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
95         try {
96             mService.initialize(
97                     mCallerAttributionSource,
98                     mUserHandle,
99                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
100                     new IAppSearchResultCallback.Stub() {
101                         @Override
102                         public void onResult(AppSearchResultParcel resultParcel) {
103                             safeExecute(executor, callback, () -> {
104                                 AppSearchResult<Void> result = resultParcel.getResult();
105                                 if (result.isSuccess()) {
106                                     callback.accept(
107                                             AppSearchResult.newSuccessfulResult(
108                                                     AppSearchSession.this));
109                                 } else {
110                                     callback.accept(AppSearchResult.newFailedResult(result));
111                                 }
112                             });
113                         }
114                     });
115         } catch (RemoteException e) {
116             throw e.rethrowFromSystemServer();
117         }
118     }
119 
AppSearchSession(@onNull IAppSearchManager service, @NonNull UserHandle userHandle, @NonNull AttributionSource callerAttributionSource, @NonNull String databaseName)120     private AppSearchSession(@NonNull IAppSearchManager service, @NonNull UserHandle userHandle,
121             @NonNull AttributionSource callerAttributionSource, @NonNull String databaseName) {
122         mService = service;
123         mUserHandle = userHandle;
124         mCallerAttributionSource = callerAttributionSource;
125         mDatabaseName = databaseName;
126     }
127 
128     /**
129      * Sets the schema that represents the organizational structure of data within the AppSearch
130      * database.
131      *
132      * <p>Upon creating an {@link AppSearchSession}, {@link #setSchema} should be called. If the
133      * schema needs to be updated, or it has not been previously set, then the provided schema will
134      * be saved and persisted to disk. Otherwise, {@link #setSchema} is handled efficiently as a
135      * no-op call.
136      *
137      * @param request the schema to set or update the AppSearch database to.
138      * @param workExecutor Executor on which to schedule heavy client-side background work such as
139      *                     transforming documents.
140      * @param callbackExecutor Executor on which to invoke the callback.
141      * @param callback Callback to receive errors resulting from setting the schema. If the
142      *                 operation succeeds, the callback will be invoked with {@code null}.
143      */
setSchema( @onNull SetSchemaRequest request, @NonNull Executor workExecutor, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback)144     public void setSchema(
145             @NonNull SetSchemaRequest request,
146             @NonNull Executor workExecutor,
147             @NonNull @CallbackExecutor Executor callbackExecutor,
148             @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
149         Objects.requireNonNull(request);
150         Objects.requireNonNull(workExecutor);
151         Objects.requireNonNull(callbackExecutor);
152         Objects.requireNonNull(callback);
153         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
154         List<Bundle> schemaBundles = new ArrayList<>(request.getSchemas().size());
155         for (AppSearchSchema schema : request.getSchemas()) {
156             schemaBundles.add(schema.getBundle());
157         }
158 
159         // Extract a List<VisibilityDocument> from the request and convert to a
160         // List<VisibilityDocument.Bundle> to send via binder.
161         List<VisibilityDocument> visibilityDocuments = VisibilityDocument
162                 .toVisibilityDocuments(request);
163         List<Bundle> visibilityBundles = new ArrayList<>(visibilityDocuments.size());
164         for (int i = 0; i < visibilityDocuments.size(); i++) {
165             visibilityBundles.add(visibilityDocuments.get(i).getBundle());
166         }
167 
168         // No need to trigger migration if user never set migrator
169         if (request.getMigrators().isEmpty()) {
170             setSchemaNoMigrations(
171                     request,
172                     schemaBundles,
173                     visibilityBundles,
174                     callbackExecutor,
175                     callback);
176         } else {
177             setSchemaWithMigrations(
178                     request,
179                     schemaBundles,
180                     visibilityBundles,
181                     workExecutor,
182                     callbackExecutor,
183                     callback);
184         }
185         mIsMutated = true;
186     }
187 
188     /**
189      * Retrieves the schema most recently successfully provided to {@link #setSchema}.
190      *
191      * @param executor Executor on which to invoke the callback.
192      * @param callback Callback to receive the pending results of schema.
193      */
getSchema( @onNull @allbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<GetSchemaResponse>> callback)194     public void getSchema(
195             @NonNull @CallbackExecutor Executor executor,
196             @NonNull Consumer<AppSearchResult<GetSchemaResponse>> callback) {
197         Objects.requireNonNull(executor);
198         Objects.requireNonNull(callback);
199         String targetPackageName =
200             Objects.requireNonNull(mCallerAttributionSource.getPackageName());
201         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
202         try {
203             mService.getSchema(
204                     mCallerAttributionSource,
205                     targetPackageName,
206                     mDatabaseName,
207                     mUserHandle,
208                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
209                     new IAppSearchResultCallback.Stub() {
210                         @Override
211                         public void onResult(AppSearchResultParcel resultParcel) {
212                             safeExecute(executor, callback, () -> {
213                                 AppSearchResult<Bundle> result = resultParcel.getResult();
214                                 if (result.isSuccess()) {
215                                     GetSchemaResponse response = new GetSchemaResponse(
216                                         Objects.requireNonNull(result.getResultValue()));
217                                     callback.accept(AppSearchResult.newSuccessfulResult(response));
218                                 } else {
219                                     callback.accept(AppSearchResult.newFailedResult(result));
220                                 }
221                             });
222                         }
223                     });
224         } catch (RemoteException e) {
225             throw e.rethrowFromSystemServer();
226         }
227     }
228 
229     /**
230      * Retrieves the set of all namespaces in the current database with at least one document.
231      *
232      * @param executor        Executor on which to invoke the callback.
233      * @param callback        Callback to receive the namespaces.
234      */
getNamespaces( @onNull @allbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<Set<String>>> callback)235     public void getNamespaces(
236             @NonNull @CallbackExecutor Executor executor,
237             @NonNull Consumer<AppSearchResult<Set<String>>> callback) {
238         Objects.requireNonNull(executor);
239         Objects.requireNonNull(callback);
240         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
241         try {
242             mService.getNamespaces(
243                     mCallerAttributionSource,
244                     mDatabaseName,
245                     mUserHandle,
246                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
247                     new IAppSearchResultCallback.Stub() {
248                         @Override
249                         public void onResult(AppSearchResultParcel resultParcel) {
250                             safeExecute(executor, callback, () -> {
251                                 AppSearchResult<List<String>> result = resultParcel.getResult();
252                                 if (result.isSuccess()) {
253                                     Set<String> namespaces =
254                                             new ArraySet<>(result.getResultValue());
255                                     callback.accept(
256                                             AppSearchResult.newSuccessfulResult(namespaces));
257                                 } else {
258                                     callback.accept(AppSearchResult.newFailedResult(result));
259                                 }
260                             });
261                         }
262                     });
263         } catch (RemoteException e) {
264             throw e.rethrowFromSystemServer();
265         }
266     }
267 
268     /**
269      * Indexes documents into the {@link AppSearchSession} database.
270      *
271      * <p>Each {@link GenericDocument} object must have a {@code schemaType} field set to an {@link
272      * AppSearchSchema} type that has been previously registered by calling the {@link #setSchema}
273      * method.
274      *
275      * @param request containing documents to be indexed.
276      * @param executor Executor on which to invoke the callback.
277      * @param callback Callback to receive pending result of performing this operation. The keys
278      *                 of the returned {@link AppSearchBatchResult} are the IDs of the input
279      *                 documents. The values are {@code null} if they were successfully indexed,
280      *                 or a failed {@link AppSearchResult} otherwise. If an unexpected internal
281      *                 error occurs in the AppSearch service,
282      *                 {@link BatchResultCallback#onSystemError} will be invoked with a
283      *                 {@link Throwable}.
284      */
put( @onNull PutDocumentsRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull BatchResultCallback<String, Void> callback)285     public void put(
286             @NonNull PutDocumentsRequest request,
287             @NonNull @CallbackExecutor Executor executor,
288             @NonNull BatchResultCallback<String, Void> callback) {
289         Objects.requireNonNull(request);
290         Objects.requireNonNull(executor);
291         Objects.requireNonNull(callback);
292         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
293         DocumentsParcel documentsParcel =
294                 new DocumentsParcel(request.getGenericDocuments());
295         try {
296             mService.putDocuments(mCallerAttributionSource, mDatabaseName, documentsParcel,
297                     mUserHandle,
298                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
299                     new IAppSearchBatchResultCallback.Stub() {
300                         @Override
301                         public void onResult(AppSearchBatchResultParcel resultParcel) {
302                             safeExecute(
303                                     executor,
304                                     callback,
305                                     () -> callback.onResult(resultParcel.getResult()));
306                         }
307 
308                         @Override
309                         public void onSystemError(AppSearchResultParcel resultParcel) {
310                             safeExecute(
311                                     executor,
312                                     callback,
313                                     () -> SearchSessionUtil.sendSystemErrorToCallback(
314                                             resultParcel.getResult(), callback));
315                         }
316                     });
317             mIsMutated = true;
318         } catch (RemoteException e) {
319             throw e.rethrowFromSystemServer();
320         }
321     }
322 
323     /**
324      * Gets {@link GenericDocument} objects by document IDs in a namespace from the {@link
325      * AppSearchSession} database.
326      *
327      * @param request a request containing a namespace and IDs to get documents for.
328      * @param executor Executor on which to invoke the callback.
329      * @param callback Callback to receive the pending result of performing this operation. The keys
330      *                 of the returned {@link AppSearchBatchResult} are the input IDs. The values
331      *                 are the returned {@link GenericDocument}s on success, or a failed
332      *                 {@link AppSearchResult} otherwise. IDs that are not found will return a
333      *                 failed {@link AppSearchResult} with a result code of
334      *                 {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
335      *                 occurs in the AppSearch service, {@link BatchResultCallback#onSystemError}
336      *                 will be invoked with a {@link Throwable}.
337      */
getByDocumentId( @onNull GetByDocumentIdRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull BatchResultCallback<String, GenericDocument> callback)338     public void getByDocumentId(
339             @NonNull GetByDocumentIdRequest request,
340             @NonNull @CallbackExecutor Executor executor,
341             @NonNull BatchResultCallback<String, GenericDocument> callback) {
342         Objects.requireNonNull(request);
343         Objects.requireNonNull(executor);
344         Objects.requireNonNull(callback);
345         String targetPackageName =
346             Objects.requireNonNull(mCallerAttributionSource.getPackageName());
347         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
348         try {
349             mService.getDocuments(
350                     mCallerAttributionSource,
351                     targetPackageName,
352                     mDatabaseName,
353                     request.getNamespace(),
354                     new ArrayList<>(request.getIds()),
355                     request.getProjectionsInternal(),
356                     mUserHandle,
357                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
358                     SearchSessionUtil.createGetDocumentCallback(executor, callback));
359         } catch (RemoteException e) {
360             throw e.rethrowFromSystemServer();
361         }
362     }
363 
364     /**
365      * Retrieves documents from the open {@link AppSearchSession} that match a given query
366      * string and type of search provided.
367      *
368      * <p>Query strings can be empty, contain one term with no operators, or contain multiple terms
369      * and operators.
370      *
371      * <p>For query strings that are empty, all documents that match the {@link SearchSpec} will be
372      * returned.
373      *
374      * <p>For query strings with a single term and no operators, documents that match the provided
375      * query string and {@link SearchSpec} will be returned.
376      *
377      * <p>The following operators are supported:
378      *
379      * <ul>
380      *   <li>AND (implicit)
381      *       <p>AND is an operator that matches documents that contain <i>all</i> provided terms.
382      *       <p><b>NOTE:</b> A space between terms is treated as an "AND" operator. Explicitly
383      *       including "AND" in a query string will treat "AND" as a term, returning documents that
384      *       also contain "AND".
385      *       <p>Example: "apple AND banana" matches documents that contain the terms "apple", "and",
386      *       "banana".
387      *       <p>Example: "apple banana" matches documents that contain both "apple" and "banana".
388      *       <p>Example: "apple banana cherry" matches documents that contain "apple", "banana", and
389      *       "cherry".
390      *   <li>OR
391      *       <p>OR is an operator that matches documents that contain <i>any</i> provided term.
392      *       <p>Example: "apple OR banana" matches documents that contain either "apple" or
393      *       "banana".
394      *       <p>Example: "apple OR banana OR cherry" matches documents that contain any of "apple",
395      *       "banana", or "cherry".
396      *   <li>Exclusion (-)
397      *       <p>Exclusion (-) is an operator that matches documents that <i>do not</i> contain the
398      *       provided term.
399      *       <p>Example: "-apple" matches documents that do not contain "apple".
400      *   <li>Grouped Terms
401      *       <p>For queries that require multiple operators and terms, terms can be grouped into
402      *       subqueries. Subqueries are contained within an open "(" and close ")" parenthesis.
403      *       <p>Example: "(donut OR bagel) (coffee OR tea)" matches documents that contain either
404      *       "donut" or "bagel" and either "coffee" or "tea".
405      *   <li>Property Restricts
406      *       <p>For queries that require a term to match a specific {@link AppSearchSchema} property
407      *       of a document, a ":" must be included between the property name and the term.
408      *       <p>Example: "subject:important" matches documents that contain the term "important" in
409      *       the "subject" property.
410      * </ul>
411      *
412      * <p>The above description covers the query operators that are supported on all versions of
413      * AppSearch. Additional operators and their required features are described below.
414      *
415      * <p>{@link Features#LIST_FILTER_QUERY_LANGUAGE}: This feature covers the expansion of the
416      * query language to conform to the definition of the list filters language (https://aip
417      * .dev/160). This includes:
418      * <ul>
419      *     <li>addition of explicit 'AND' and 'NOT' operators</li>
420      *     <li>property restricts are allowed with groupings (ex. "prop:(a OR b)")</li>
421      *     <li>addition of custom functions to control matching</li>
422      * </ul>
423      *
424      * <p>The newly added custom functions covered by this feature are:
425      * <ul>
426      *     <li>createList(String...)</li>
427      *     <li>search(String, List<String>)</li>
428      *     <li>propertyDefined(String)</li>
429      * </ul>
430      *
431      * <p>createList takes a variable number of strings and returns a list of strings.
432      * It is for use with search.
433      *
434      * <p>search takes a query string that will be parsed according to the supported
435      * query language and an optional list of strings that specify the properties to be
436      * restricted to. This exists as a convenience for multiple property restricts. So,
437      * for example, the query `(subject:foo OR body:foo) (subject:bar OR body:bar)`
438      * could be rewritten as `search("foo bar", createList("subject", "bar"))`.
439      *
440      * <p>propertyDefined takes a string specifying the property of interest and matches all
441      * documents of any type that defines the specified property
442      * (ex. `propertyDefined("sender.name")`). Note that propertyDefined will match so long as
443      * the document's type defines the specified property. It does NOT require that the document
444      * actually hold any values for this property.
445      *
446      * <p>{@link Features#NUMERIC_SEARCH}: This feature covers numeric search expressions. In the
447      * query language, the values of properties that have
448      * {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} set can be matched with a
449      * numeric search expression (the property, a supported comparator and an integer value).
450      * Supported comparators are <, <=, ==, >= and >.
451      *
452      * <p>Ex. `price < 10` will match all documents that has a numeric value in its price
453      * property that is less than 10.
454      *
455      * <p>{@link Features#VERBATIM_SEARCH}: This feature covers the verbatim string operator
456      * (quotation marks).
457      *
458      * <p>Ex. `"foo/bar" OR baz` will ensure that 'foo/bar' is treated as a single 'verbatim' token.
459      *
460      * <p>The availability of each of these features can be checked by calling
461      * {@link Features#isFeatureSupported} with the desired feature.
462      *
463      * <p>Additional search specifications, such as filtering by {@link AppSearchSchema} type or
464      * adding projection, can be set by calling the corresponding {@link SearchSpec.Builder} setter.
465      *
466      * <p>This method is lightweight. The heavy work will be done in {@link
467      * SearchResults#getNextPage}.
468      *
469      * @param queryExpression query string to search.
470      * @param searchSpec spec for setting document filters, adding projection, setting term match
471      *     type, etc.
472      * @return a {@link SearchResults} object for retrieved matched documents.
473      */
474     @NonNull
search(@onNull String queryExpression, @NonNull SearchSpec searchSpec)475     public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
476         Objects.requireNonNull(queryExpression);
477         Objects.requireNonNull(searchSpec);
478         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
479         return new SearchResults(mService, mCallerAttributionSource, mDatabaseName, queryExpression,
480                 searchSpec, mUserHandle);
481     }
482 
483     /**
484      * Retrieves suggested Strings that could be used as {@code queryExpression} in
485      * {@link #search(String, SearchSpec)} API.
486      *
487      * <p>The {@code suggestionQueryExpression} can contain one term with no operators, or contain
488      * multiple terms and operators. Operators will be considered as a normal term. Please see the
489      * operator examples below. The {@code suggestionQueryExpression} must end with a valid term,
490      * the suggestions are generated based on the last term. If the input
491      * {@code suggestionQueryExpression} doesn't have a valid token, AppSearch will return an
492      * empty result list. Please see the invalid examples below.
493      *
494      * <p>Example: if there are following documents with content stored in AppSearch.
495      * <ul>
496      *     <li>document1: "term1"
497      *     <li>document2: "term1 term2"
498      *     <li>document3: "term1 term2 term3"
499      *     <li>document4: "org"
500      * </ul>
501      *
502      * <p>Search suggestions with the single term {@code suggestionQueryExpression} "t", the
503      * suggested results are:
504      * <ul>
505      *     <li>"term1" - Use it to be queryExpression in {@link #search} could get 3
506      *     {@link SearchResult}s, which contains document 1, 2 and 3.
507      *     <li>"term2" - Use it to be queryExpression in {@link #search} could get 2
508      *     {@link SearchResult}s, which contains document 2 and 3.
509      *     <li>"term3" - Use it to be queryExpression in {@link #search} could get 1
510      *     {@link SearchResult}, which contains document 3.
511      * </ul>
512      *
513      * <p>Search suggestions with the multiple term {@code suggestionQueryExpression} "org t", the
514      * suggested result will be "org term1" - The last token is completed by the suggested
515      * String.
516      *
517      * <p>Operators in {@link #search} are supported.
518      * <p><b>NOTE:</b> Exclusion and Grouped Terms in the last term is not supported.
519      * <p>example: "apple -f": This Api will throw an
520      * {@link android.app.appsearch.exceptions.AppSearchException} with
521      * {@link AppSearchResult#RESULT_INVALID_ARGUMENT}.
522      * <p>example: "apple (f)": This Api will return an empty results.
523      *
524      * <p>Invalid example: All these input {@code suggestionQueryExpression} don't have a valid
525      * last token, AppSearch will return an empty result list.
526      * <ul>
527      *     <li>""      - Empty {@code suggestionQueryExpression}.
528      *     <li>"(f)"   - Ending in a closed brackets.
529      *     <li>"f:"    - Ending in an operator.
530      *     <li>"f    " - Ending in trailing space.
531      * </ul>
532      *
533      * @param suggestionQueryExpression the non empty query string to search suggestions
534      * @param searchSuggestionSpec      spec for setting document filters
535      * @param executor Executor on which to invoke the callback.
536      * @param callback Callback to receive the pending result of performing this operation, which
537      *                 is a List of {@link SearchSuggestionResult} on success. The returned
538      *                 suggestion Strings are ordered by the number of {@link SearchResult} you
539      *                 could get by using that suggestion in {@link #search}.
540      *
541      * @see #search(String, SearchSpec)
542      */
searchSuggestion( @onNull String suggestionQueryExpression, @NonNull SearchSuggestionSpec searchSuggestionSpec, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<List<SearchSuggestionResult>>> callback)543     public void searchSuggestion(
544             @NonNull String suggestionQueryExpression,
545             @NonNull SearchSuggestionSpec searchSuggestionSpec,
546             @NonNull @CallbackExecutor Executor executor,
547             @NonNull Consumer<AppSearchResult<List<SearchSuggestionResult>>> callback) {
548         Objects.requireNonNull(suggestionQueryExpression);
549         Objects.requireNonNull(searchSuggestionSpec);
550         Objects.requireNonNull(executor);
551         Objects.requireNonNull(callback);
552         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
553         try {
554             mService.searchSuggestion(
555                     mCallerAttributionSource,
556                     mDatabaseName,
557                     suggestionQueryExpression,
558                     searchSuggestionSpec.getBundle(),
559                     mUserHandle,
560                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
561                     new IAppSearchResultCallback.Stub() {
562                         @Override
563                         public void onResult(AppSearchResultParcel resultParcel) {
564                             safeExecute(executor, callback, () -> {
565                                 try {
566                                     AppSearchResult<List<Bundle>> result = resultParcel.getResult();
567                                     if (result.isSuccess()) {
568                                         List<Bundle> suggestionResultBundles =
569                                                 result.getResultValue();
570                                         List<SearchSuggestionResult> searchSuggestionResults =
571                                                 new ArrayList<>(suggestionResultBundles.size());
572                                         for (int i = 0; i < suggestionResultBundles.size(); i++) {
573                                             SearchSuggestionResult searchSuggestionResult =
574                                                     new SearchSuggestionResult(
575                                                             suggestionResultBundles.get(i));
576                                             searchSuggestionResults.add(searchSuggestionResult);
577                                         }
578                                         callback.accept(
579                                                 AppSearchResult.newSuccessfulResult(
580                                                         searchSuggestionResults));
581                                     } else {
582                                         // TODO(b/261897334) save SDK errors/crashes and send to
583                                         //  server for logging.
584                                         callback.accept(AppSearchResult.newFailedResult(result));
585                                     }
586                                 } catch (Exception e) {
587                                     callback.accept(AppSearchResult.throwableToFailedResult(e));
588                                 }
589                             });
590                         }
591                     }
592             );
593         } catch (RemoteException e) {
594             throw e.rethrowFromSystemServer();
595         }
596     }
597 
598     /**
599      * Reports usage of a particular document by namespace and ID.
600      *
601      * <p>A usage report represents an event in which a user interacted with or viewed a document.
602      *
603      * <p>For each call to {@link #reportUsage}, AppSearch updates usage count and usage recency
604      * metrics for that particular document. These metrics are used for ordering {@link #search}
605      * results by the {@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} and {@link
606      * SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} ranking strategies.
607      *
608      * <p>Reporting usage of a document is optional.
609      *
610      * @param request The usage reporting request.
611      * @param executor Executor on which to invoke the callback.
612      * @param callback Callback to receive errors. If the operation succeeds, the callback will be
613      *                 invoked with {@code null}.
614      */
reportUsage( @onNull ReportUsageRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<Void>> callback)615     public void reportUsage(
616             @NonNull ReportUsageRequest request,
617             @NonNull @CallbackExecutor Executor executor,
618             @NonNull Consumer<AppSearchResult<Void>> callback) {
619         Objects.requireNonNull(request);
620         Objects.requireNonNull(executor);
621         Objects.requireNonNull(callback);
622         String targetPackageName =
623             Objects.requireNonNull(mCallerAttributionSource.getPackageName());
624         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
625         try {
626             mService.reportUsage(
627                     mCallerAttributionSource,
628                     targetPackageName,
629                     mDatabaseName,
630                     request.getNamespace(),
631                     request.getDocumentId(),
632                     request.getUsageTimestampMillis(),
633                     /*systemUsage=*/ false,
634                     mUserHandle,
635                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
636                     new IAppSearchResultCallback.Stub() {
637                         @Override
638                         public void onResult(AppSearchResultParcel resultParcel) {
639                             safeExecute(
640                                     executor,
641                                     callback,
642                                     () -> callback.accept(resultParcel.getResult()));
643                         }
644                     });
645             mIsMutated = true;
646         } catch (RemoteException e) {
647             throw e.rethrowFromSystemServer();
648         }
649     }
650 
651     /**
652      * Removes {@link GenericDocument} objects by document IDs in a namespace from the {@link
653      * AppSearchSession} database.
654      *
655      * <p>Removed documents will no longer be surfaced by {@link #search} or {@link
656      * #getByDocumentId} calls.
657      *
658      * <p>Once the database crosses the document count or byte usage threshold, removed documents
659      * will be deleted from disk.
660      *
661      * @param request {@link RemoveByDocumentIdRequest} with IDs in a namespace to remove from the
662      *     index.
663      * @param executor Executor on which to invoke the callback.
664      * @param callback Callback to receive the pending result of performing this operation. The keys
665      *                 of the returned {@link AppSearchBatchResult} are the input document IDs. The
666      *                 values are {@code null} on success, or a failed {@link AppSearchResult}
667      *                 otherwise. IDs that are not found will return a failed
668      *                 {@link AppSearchResult} with a result code of
669      *                 {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
670      *                 occurs in the AppSearch service, {@link BatchResultCallback#onSystemError}
671      *                 will be invoked with a {@link Throwable}.
672      */
remove( @onNull RemoveByDocumentIdRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull BatchResultCallback<String, Void> callback)673     public void remove(
674             @NonNull RemoveByDocumentIdRequest request,
675             @NonNull @CallbackExecutor Executor executor,
676             @NonNull BatchResultCallback<String, Void> callback) {
677         Objects.requireNonNull(request);
678         Objects.requireNonNull(executor);
679         Objects.requireNonNull(callback);
680         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
681         try {
682             mService.removeByDocumentId(
683                     mCallerAttributionSource,
684                     mDatabaseName,
685                     request.getNamespace(),
686                     new ArrayList<>(request.getIds()),
687                     mUserHandle,
688                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
689                     new IAppSearchBatchResultCallback.Stub() {
690                         @Override
691                         public void onResult(AppSearchBatchResultParcel resultParcel) {
692                             safeExecute(
693                                     executor,
694                                     callback,
695                                     () -> callback.onResult(resultParcel.getResult()));
696                         }
697 
698                         @Override
699                         public void onSystemError(AppSearchResultParcel resultParcel) {
700                             safeExecute(
701                                     executor,
702                                     callback,
703                                     () -> SearchSessionUtil.sendSystemErrorToCallback(
704                                             resultParcel.getResult(), callback));
705                         }
706                     });
707             mIsMutated = true;
708         } catch (RemoteException e) {
709             throw e.rethrowFromSystemServer();
710         }
711     }
712 
713     /**
714      * Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
715      * match the {@code queryExpression} in given namespaces and schemaTypes which is set via {@link
716      * SearchSpec.Builder#addFilterNamespaces} and {@link SearchSpec.Builder#addFilterSchemas}.
717      *
718      * <p>An empty {@code queryExpression} matches all documents.
719      *
720      * <p>An empty set of namespaces or schemaTypes matches all namespaces or schemaTypes in the
721      * current database.
722      *
723      * @param queryExpression Query String to search.
724      * @param searchSpec Spec containing schemaTypes, namespaces and query expression indicates how
725      *     document will be removed. All specific about how to scoring, ordering, snippeting and
726      *     resulting will be ignored.
727      * @param executor        Executor on which to invoke the callback.
728      * @param callback        Callback to receive errors resulting from removing the documents. If
729      *                        the operation succeeds, the callback will be invoked with
730      *                        {@code null}.
731      */
remove( @onNull String queryExpression, @NonNull SearchSpec searchSpec, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<Void>> callback)732     public void remove(
733             @NonNull String queryExpression,
734             @NonNull SearchSpec searchSpec,
735             @NonNull @CallbackExecutor Executor executor,
736             @NonNull Consumer<AppSearchResult<Void>> callback) {
737         Objects.requireNonNull(queryExpression);
738         Objects.requireNonNull(searchSpec);
739         Objects.requireNonNull(executor);
740         Objects.requireNonNull(callback);
741         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
742         if (searchSpec.getJoinSpec() != null) {
743             throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
744                     + "JoinSpec was provided.");
745         }
746         try {
747             mService.removeByQuery(
748                     mCallerAttributionSource,
749                     mDatabaseName,
750                     queryExpression,
751                     searchSpec.getBundle(),
752                     mUserHandle,
753                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
754                     new IAppSearchResultCallback.Stub() {
755                         @Override
756                         public void onResult(AppSearchResultParcel resultParcel) {
757                             safeExecute(
758                                     executor,
759                                     callback,
760                                     () -> callback.accept(resultParcel.getResult()));
761                         }
762                     });
763             mIsMutated = true;
764         } catch (RemoteException e) {
765             throw e.rethrowFromSystemServer();
766         }
767     }
768 
769     /**
770      * Gets the storage info for this {@link AppSearchSession} database.
771      *
772      * <p>This may take time proportional to the number of documents and may be inefficient to call
773      * repeatedly.
774      *
775      * @param executor        Executor on which to invoke the callback.
776      * @param callback        Callback to receive the storage info.
777      */
getStorageInfo( @onNull @allbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<StorageInfo>> callback)778     public void getStorageInfo(
779             @NonNull @CallbackExecutor Executor executor,
780             @NonNull Consumer<AppSearchResult<StorageInfo>> callback) {
781         Objects.requireNonNull(executor);
782         Objects.requireNonNull(callback);
783         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
784         try {
785             mService.getStorageInfo(
786                     mCallerAttributionSource,
787                     mDatabaseName,
788                     mUserHandle,
789                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
790                     new IAppSearchResultCallback.Stub() {
791                         @Override
792                         public void onResult(AppSearchResultParcel resultParcel) {
793                             safeExecute(executor, callback, () -> {
794                                 AppSearchResult<Bundle> result = resultParcel.getResult();
795                                 if (result.isSuccess()) {
796                                     StorageInfo response = new StorageInfo(
797                                         Objects.requireNonNull(result.getResultValue()));
798                                     callback.accept(AppSearchResult.newSuccessfulResult(response));
799                                 } else {
800                                     callback.accept(AppSearchResult.newFailedResult(result));
801                                 }
802                             });
803                         }
804                     });
805         } catch (RemoteException e) {
806             throw e.rethrowFromSystemServer();
807         }
808     }
809 
810     /**
811      * Closes the {@link AppSearchSession} to persist all schema and document updates,
812      * additions, and deletes to disk.
813      */
814     @Override
close()815     public void close() {
816         if (mIsMutated && !mIsClosed) {
817             try {
818                 mService.persistToDisk(
819                         mCallerAttributionSource,
820                         mUserHandle,
821                         /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime());
822                 mIsClosed = true;
823             } catch (RemoteException e) {
824                 Log.e(TAG, "Unable to close the AppSearchSession", e);
825             }
826         }
827     }
828 
829     /**
830      * Set schema to Icing for no-migration scenario.
831      *
832      * <p>We only need one time {@link #setSchema} call for no-migration scenario by using the
833      * forceoverride in the request.
834      */
setSchemaNoMigrations( @onNull SetSchemaRequest request, @NonNull List<Bundle> schemaBundles, @NonNull List<Bundle> visibilityBundles, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback)835     private void setSchemaNoMigrations(
836             @NonNull SetSchemaRequest request,
837             @NonNull List<Bundle> schemaBundles,
838             @NonNull List<Bundle> visibilityBundles,
839             @NonNull @CallbackExecutor Executor executor,
840             @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
841         try {
842             mService.setSchema(
843                     mCallerAttributionSource,
844                     mDatabaseName,
845                     schemaBundles,
846                     visibilityBundles,
847                     request.isForceOverride(),
848                     request.getVersion(),
849                     mUserHandle,
850                     /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
851                     SchemaMigrationStats.NO_MIGRATION,
852                     new IAppSearchResultCallback.Stub() {
853                         @Override
854                         public void onResult(AppSearchResultParcel resultParcel) {
855                             safeExecute(executor, callback, () -> {
856                                 AppSearchResult<Bundle> result = resultParcel.getResult();
857                                 if (result.isSuccess()) {
858                                     try {
859                                         InternalSetSchemaResponse internalSetSchemaResponse =
860                                                 new InternalSetSchemaResponse(
861                                                         result.getResultValue());
862                                         if (!internalSetSchemaResponse.isSuccess()) {
863                                             // check is the set schema call failed because
864                                             // incompatible changes. That's the only case we
865                                             // swallowed in the AppSearchImpl#setSchema().
866                                             callback.accept(AppSearchResult.newFailedResult(
867                                                     AppSearchResult.RESULT_INVALID_SCHEMA,
868                                                     internalSetSchemaResponse.getErrorMessage()));
869                                             return;
870                                         }
871                                         callback.accept(AppSearchResult.newSuccessfulResult(
872                                                 internalSetSchemaResponse.getSetSchemaResponse()));
873                                     } catch (Throwable t) {
874                                         // TODO(b/261897334) save SDK errors/crashes and send to
875                                         //  server for logging.
876                                         callback.accept(AppSearchResult.throwableToFailedResult(t));
877                                     }
878                                 } else {
879                                     callback.accept(AppSearchResult.newFailedResult(result));
880                                 }
881                             });
882                         }
883                     });
884         } catch (RemoteException e) {
885             throw e.rethrowFromSystemServer();
886         }
887     }
888 
889     /**
890      * Set schema to Icing for migration scenario.
891      *
892      * <p>First time {@link #setSchema} call with forceOverride is false gives us all incompatible
893      * changes. After trigger migrations, the second time call {@link #setSchema} will actually
894      * apply the changes.
895      */
setSchemaWithMigrations( @onNull SetSchemaRequest request, @NonNull List<Bundle> schemaBundles, @NonNull List<Bundle> visibilityBundles, @NonNull Executor workExecutor, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback)896     private void setSchemaWithMigrations(
897             @NonNull SetSchemaRequest request,
898             @NonNull List<Bundle> schemaBundles,
899             @NonNull List<Bundle> visibilityBundles,
900             @NonNull Executor workExecutor,
901             @NonNull @CallbackExecutor Executor callbackExecutor,
902             @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
903         long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
904         long waitExecutorStartLatencyMillis = SystemClock.elapsedRealtime();
905         safeExecute(workExecutor, callback, () -> {
906             try {
907                 long waitExecutorEndLatencyMillis = SystemClock.elapsedRealtime();
908                 SchemaMigrationStats.Builder statsBuilder = new SchemaMigrationStats.Builder(
909                         mCallerAttributionSource.getPackageName(), mDatabaseName);
910 
911                 // Migration process
912                 // 1. Validate and retrieve all active migrators.
913                 long getSchemaLatencyStartTimeMillis = SystemClock.elapsedRealtime();
914                 CountDownLatch getSchemaLatch = new CountDownLatch(1);
915                 AtomicReference<AppSearchResult<GetSchemaResponse>> getSchemaResultRef =
916                         new AtomicReference<>();
917                 getSchema(callbackExecutor, (result) -> {
918                     getSchemaResultRef.set(result);
919                     getSchemaLatch.countDown();
920                 });
921                 getSchemaLatch.await();
922                 AppSearchResult<GetSchemaResponse> getSchemaResult = getSchemaResultRef.get();
923                 if (!getSchemaResult.isSuccess()) {
924                     // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
925                     safeExecute(
926                             callbackExecutor,
927                             callback,
928                             () -> callback.accept(
929                                     AppSearchResult.newFailedResult(getSchemaResult)));
930                     return;
931                 }
932                 GetSchemaResponse getSchemaResponse =
933                         Objects.requireNonNull(getSchemaResult.getResultValue());
934                 int currentVersion = getSchemaResponse.getVersion();
935                 int finalVersion = request.getVersion();
936                 Map<String, Migrator> activeMigrators = SchemaMigrationUtil.getActiveMigrators(
937                         getSchemaResponse.getSchemas(), request.getMigrators(), currentVersion,
938                         finalVersion);
939                 long getSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
940 
941                 // No need to trigger migration if no migrator is active.
942                 if (activeMigrators.isEmpty()) {
943                     setSchemaNoMigrations(request, schemaBundles, visibilityBundles,
944                             callbackExecutor, callback);
945                     return;
946                 }
947 
948                 // 2. SetSchema with forceOverride=false, to retrieve the list of
949                 // incompatible/deleted types.
950                 long firstSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
951                 CountDownLatch setSchemaLatch = new CountDownLatch(1);
952                 AtomicReference<AppSearchResult<Bundle>> setSchemaResultRef =
953                         new AtomicReference<>();
954 
955                 mService.setSchema(
956                         mCallerAttributionSource,
957                         mDatabaseName,
958                         schemaBundles,
959                         visibilityBundles,
960                         /*forceOverride=*/ false,
961                         request.getVersion(),
962                         mUserHandle,
963                         /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
964                         SchemaMigrationStats.FIRST_CALL_GET_INCOMPATIBLE,
965                         new IAppSearchResultCallback.Stub() {
966                             @Override
967                             public void onResult(AppSearchResultParcel resultParcel) {
968                                 setSchemaResultRef.set(resultParcel.getResult());
969                                 setSchemaLatch.countDown();
970                             }
971                         });
972                 setSchemaLatch.await();
973                 AppSearchResult<Bundle> setSchemaResult = setSchemaResultRef.get();
974                 if (!setSchemaResult.isSuccess()) {
975                     // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
976                     safeExecute(
977                             callbackExecutor,
978                             callback,
979                             () -> callback.accept(
980                                     AppSearchResult.newFailedResult(setSchemaResult)));
981                     return;
982                 }
983                 InternalSetSchemaResponse internalSetSchemaResponse1 =
984                         new InternalSetSchemaResponse(setSchemaResult.getResultValue());
985                 long firstSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
986 
987                 // 3. If forceOverride is false, check that all incompatible types will be migrated.
988                 // If some aren't we must throw an error, rather than proceeding and deleting those
989                 // types.
990                 SchemaMigrationUtil.checkDeletedAndIncompatibleAfterMigration(
991                         internalSetSchemaResponse1, activeMigrators.keySet());
992 
993                 try (AppSearchMigrationHelper migrationHelper = new AppSearchMigrationHelper(
994                         mService, mUserHandle, mCallerAttributionSource, mDatabaseName,
995                         request.getSchemas())) {
996 
997                     // 4. Trigger migration for all migrators.
998                     long queryAndTransformLatencyStartTimeMillis = SystemClock.elapsedRealtime();
999                     for (Map.Entry<String, Migrator> entry : activeMigrators.entrySet()) {
1000                         migrationHelper.queryAndTransform(/*schemaType=*/ entry.getKey(),
1001                                 /*migrator=*/ entry.getValue(), currentVersion,
1002                                 finalVersion, statsBuilder);
1003                     }
1004                     long queryAndTransformLatencyEndTimeMillis = SystemClock.elapsedRealtime();
1005 
1006                     // 5. SetSchema a second time with forceOverride=true if the first attempted
1007                     // failed.
1008                     long secondSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
1009                     InternalSetSchemaResponse internalSetSchemaResponse;
1010                     if (internalSetSchemaResponse1.isSuccess()) {
1011                         internalSetSchemaResponse = internalSetSchemaResponse1;
1012                     } else {
1013                         CountDownLatch setSchema2Latch = new CountDownLatch(1);
1014                         AtomicReference<AppSearchResult<Bundle>> setSchema2ResultRef =
1015                                 new AtomicReference<>();
1016                         // only trigger second setSchema() call if the first one is fail.
1017                         mService.setSchema(
1018                                 mCallerAttributionSource,
1019                                 mDatabaseName,
1020                                 schemaBundles,
1021                                 visibilityBundles,
1022                                 /*forceOverride=*/ true,
1023                                 request.getVersion(),
1024                                 mUserHandle,
1025                                 /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
1026                                 SchemaMigrationStats.SECOND_CALL_APPLY_NEW_SCHEMA,
1027                                 new IAppSearchResultCallback.Stub() {
1028                                     @Override
1029                                     public void onResult(AppSearchResultParcel resultParcel) {
1030                                         setSchema2ResultRef.set(resultParcel.getResult());
1031                                         setSchema2Latch.countDown();
1032                                     }
1033                                 });
1034                         setSchema2Latch.await();
1035                         AppSearchResult<Bundle> setSchema2Result = setSchema2ResultRef.get();
1036                         if (!setSchema2Result.isSuccess()) {
1037                             // we failed to set the schema in second time with forceOverride = true,
1038                             // which is an impossible case. Since we only swallow the incompatible
1039                             // error in the first setSchema call, all other errors will be thrown at
1040                             // the first time.
1041                             // TODO(b/261897334) save SDK errors/crashes and send to server for
1042                             //  logging.
1043                             safeExecute(
1044                                     callbackExecutor,
1045                                     callback,
1046                                     () -> callback.accept(
1047                                             AppSearchResult.newFailedResult(setSchema2Result)));
1048                             return;
1049                         }
1050                         InternalSetSchemaResponse internalSetSchemaResponse2 =
1051                                 new InternalSetSchemaResponse(setSchema2Result.getResultValue());
1052                         if (!internalSetSchemaResponse2.isSuccess()) {
1053                             // Impossible case, we just set forceOverride to be true, we should
1054                             // never fail in incompatible changes. And all other cases should failed
1055                             // during the first call.
1056                             // TODO(b/261897334) save SDK errors/crashes and send to server for
1057                             //  logging.
1058                             safeExecute(
1059                                     callbackExecutor,
1060                                     callback,
1061                                     () -> callback.accept(
1062                                             AppSearchResult.newFailedResult(
1063                                                     AppSearchResult.RESULT_INTERNAL_ERROR,
1064                                                     internalSetSchemaResponse2.getErrorMessage())));
1065                             return;
1066                         }
1067                         internalSetSchemaResponse = internalSetSchemaResponse2;
1068                     }
1069                     long secondSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
1070 
1071                     statsBuilder
1072                             .setExecutorAcquisitionLatencyMillis(
1073                                     (int) (waitExecutorEndLatencyMillis
1074                                             - waitExecutorStartLatencyMillis))
1075                             .setGetSchemaLatencyMillis(
1076                                     (int)(getSchemaLatencyEndTimeMillis
1077                                             - getSchemaLatencyStartTimeMillis))
1078                             .setFirstSetSchemaLatencyMillis(
1079                                     (int)(firstSetSchemaLatencyEndTimeMillis
1080                                             - firstSetSchemaLatencyStartMillis))
1081                             .setIsFirstSetSchemaSuccess(internalSetSchemaResponse1.isSuccess())
1082                             .setQueryAndTransformLatencyMillis(
1083                                     (int)(queryAndTransformLatencyEndTimeMillis -
1084                                             queryAndTransformLatencyStartTimeMillis))
1085                             .setSecondSetSchemaLatencyMillis(
1086                                     (int)(secondSetSchemaLatencyEndTimeMillis
1087                                             - secondSetSchemaLatencyStartMillis));
1088                     SetSchemaResponse.Builder responseBuilder = internalSetSchemaResponse
1089                             .getSetSchemaResponse()
1090                             .toBuilder()
1091                             .addMigratedTypes(activeMigrators.keySet());
1092 
1093                     // 6. Put all the migrated documents into the index, now that the new schema is
1094                     // set.
1095                     AppSearchResult<SetSchemaResponse> putResult =
1096                             migrationHelper.putMigratedDocuments(
1097                                     responseBuilder, statsBuilder, totalLatencyStartTimeMillis);
1098                     safeExecute(callbackExecutor, callback, () -> callback.accept(putResult));
1099                 }
1100             } catch (Throwable t) {
1101                 // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
1102                 safeExecute(
1103                         callbackExecutor,
1104                         callback,
1105                         () -> callback.accept(AppSearchResult.throwableToFailedResult(t)));
1106             }
1107         });
1108     }
1109 }
1110