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