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