1 /* 2 * Copyright (C) 2021 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 com.android.server.appsearch.contactsindexer; 18 19 import static com.android.server.appsearch.indexer.IndexerMaintenanceConfig.CONTACTS_INDEXER; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.appsearch.AppSearchEnvironmentFactory; 24 import android.app.appsearch.AppSearchResult; 25 import android.app.appsearch.util.LogUtil; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.database.ContentObserver; 29 import android.net.Uri; 30 import android.os.CancellationSignal; 31 import android.provider.ContactsContract; 32 import android.util.Log; 33 import android.util.Slog; 34 35 import com.android.internal.annotations.GuardedBy; 36 import com.android.internal.annotations.VisibleForTesting; 37 import com.android.server.appsearch.indexer.IndexerMaintenanceService; 38 import com.android.server.appsearch.stats.AppSearchStatsLog; 39 40 import java.io.File; 41 import java.io.FileNotFoundException; 42 import java.io.IOException; 43 import java.io.PrintWriter; 44 import java.util.ArrayList; 45 import java.util.Collection; 46 import java.util.List; 47 import java.util.Objects; 48 import java.util.concurrent.CompletableFuture; 49 import java.util.concurrent.ExecutorService; 50 import java.util.concurrent.TimeUnit; 51 52 /** 53 * Contacts Indexer for a single user. 54 * 55 * <p>It reads the updated/newly-inserted/deleted contacts from CP2, and sync the changes into 56 * AppSearch. 57 * 58 * <p>This class is thread safe. 59 * 60 * @hide 61 */ 62 public final class ContactsIndexerUserInstance { 63 64 private static final String TAG = "ContactsIndexerUserInst"; 65 66 private final Context mContext; 67 private final File mDataDir; 68 private final ContactsIndexerSettings mSettings; 69 private final ContactsObserver mContactsObserver; 70 71 // Those two booleans below are used for batching/throttling the contact change 72 // notification so we won't schedule too many delta updates. 73 private final Object mDeltaUpdateLock = new Object(); 74 75 // Whether a delta update has been scheduled or run. Now we only allow one delta update being 76 // run at a time. 77 @GuardedBy("mDeltaUpdateLock") 78 private boolean mDeltaUpdateScheduled = false; 79 80 // Whether we are receiving notifications from CP2. 81 @GuardedBy("mDeltaUpdateLock") 82 private boolean mCp2ChangePending = false; 83 84 private final AppSearchHelper mAppSearchHelper; 85 private final ContactsIndexerImpl mContactsIndexerImpl; 86 private final ContactsIndexerConfig mContactsIndexerConfig; 87 88 /** 89 * Single threaded executor to make sure there is only one active sync for this {@link 90 * ContactsIndexerUserInstance}. Background tasks should be scheduled using {@link 91 * #executeOnSingleThreadedExecutor(Runnable)} which ensures that they are not executed if the 92 * executor is shutdown during {@link #shutdown()}. 93 * 94 * <p>Note that this executor is used as both work and callback executors by {@link 95 * #mAppSearchHelper} which is fine because AppSearch should be able to handle exceptions thrown 96 * by them. 97 */ 98 private final ExecutorService mSingleThreadedExecutor; 99 100 /** 101 * Constructs and initializes a {@link ContactsIndexerUserInstance}. 102 * 103 * <p>Heavy operations such as connecting to AppSearch are performed asynchronously. 104 * 105 * @param contactsDir data directory for ContactsIndexer. 106 */ 107 @NonNull createInstance( @onNull Context userContext, @NonNull File contactsDir, @NonNull ContactsIndexerConfig contactsIndexerConfig)108 public static ContactsIndexerUserInstance createInstance( 109 @NonNull Context userContext, 110 @NonNull File contactsDir, 111 @NonNull ContactsIndexerConfig contactsIndexerConfig) { 112 Objects.requireNonNull(userContext); 113 Objects.requireNonNull(contactsDir); 114 Objects.requireNonNull(contactsIndexerConfig); 115 116 ExecutorService singleThreadedExecutor = 117 AppSearchEnvironmentFactory.getEnvironmentInstance().createSingleThreadExecutor(); 118 return createInstance( 119 userContext, contactsDir, contactsIndexerConfig, singleThreadedExecutor); 120 } 121 122 @VisibleForTesting 123 @NonNull createInstance( @onNull Context context, @NonNull File contactsDir, @NonNull ContactsIndexerConfig contactsIndexerConfig, @NonNull ExecutorService executorService)124 /*package*/ static ContactsIndexerUserInstance createInstance( 125 @NonNull Context context, 126 @NonNull File contactsDir, 127 @NonNull ContactsIndexerConfig contactsIndexerConfig, 128 @NonNull ExecutorService executorService) { 129 Objects.requireNonNull(context); 130 Objects.requireNonNull(contactsDir); 131 Objects.requireNonNull(contactsIndexerConfig); 132 Objects.requireNonNull(executorService); 133 134 AppSearchHelper appSearchHelper = 135 AppSearchHelper.createAppSearchHelper( 136 context, executorService, contactsIndexerConfig); 137 ContactsIndexerUserInstance indexer = 138 new ContactsIndexerUserInstance( 139 context, 140 contactsDir, 141 appSearchHelper, 142 contactsIndexerConfig, 143 executorService); 144 indexer.loadSettingsAsync(); 145 146 return indexer; 147 } 148 149 /** 150 * Constructs a {@link ContactsIndexerUserInstance}. 151 * 152 * @param context Context object passed from {@link ContactsIndexerManagerService} 153 * @param dataDir data directory for storing contacts indexer state. 154 * @param contactsIndexerConfig configuration for the Contacts Indexer. 155 * @param singleThreadedExecutor an {@link ExecutorService} with at most one thread to ensure 156 * the thread safety of this class. 157 */ ContactsIndexerUserInstance( @onNull Context context, @NonNull File dataDir, @NonNull AppSearchHelper appSearchHelper, @NonNull ContactsIndexerConfig contactsIndexerConfig, @NonNull ExecutorService singleThreadedExecutor)158 private ContactsIndexerUserInstance( 159 @NonNull Context context, 160 @NonNull File dataDir, 161 @NonNull AppSearchHelper appSearchHelper, 162 @NonNull ContactsIndexerConfig contactsIndexerConfig, 163 @NonNull ExecutorService singleThreadedExecutor) { 164 mContext = Objects.requireNonNull(context); 165 mDataDir = Objects.requireNonNull(dataDir); 166 mContactsIndexerConfig = Objects.requireNonNull(contactsIndexerConfig); 167 mSettings = new ContactsIndexerSettings(mDataDir); 168 mAppSearchHelper = Objects.requireNonNull(appSearchHelper); 169 mSingleThreadedExecutor = Objects.requireNonNull(singleThreadedExecutor); 170 mContactsObserver = new ContactsObserver(); 171 mContactsIndexerImpl = new ContactsIndexerImpl(context, appSearchHelper); 172 } 173 startAsync()174 public void startAsync() { 175 if (LogUtil.DEBUG) { 176 Log.d(TAG, "Registering ContactsObserver for " + mContext.getUser()); 177 } 178 mContext.getContentResolver() 179 .registerContentObserver( 180 ContactsContract.Contacts.CONTENT_URI, 181 /* notifyForDescendants= */ true, 182 mContactsObserver); 183 184 executeOnSingleThreadedExecutor( 185 () -> { 186 mAppSearchHelper 187 .isDataLikelyWipedDuringInitAsync() 188 .thenCompose( 189 isDataLikelyWipedDuringInit -> { 190 if (isDataLikelyWipedDuringInit) { 191 mSettings.reset(); 192 // Persist the settings right away just in case there is 193 // a crash later. 194 // In this case, the full update still need to be run 195 // during the next 196 // boot to reindex the data. 197 persistSettings(); 198 } 199 doCp2SyncFirstRun(); 200 // This value won't be used anymore, so return null here. 201 return CompletableFuture.completedFuture(null); 202 }) 203 .exceptionally(e -> Log.w(TAG, "Got exception in startAsync", e)); 204 }); 205 } 206 shutdown()207 public void shutdown() throws InterruptedException { 208 if (LogUtil.DEBUG) { 209 Log.d(TAG, "Unregistering ContactsObserver for " + mContext.getUser()); 210 } 211 mContext.getContentResolver().unregisterContentObserver(mContactsObserver); 212 213 IndexerMaintenanceService.cancelUpdateJobIfScheduled( 214 mContext, mContext.getUser(), CONTACTS_INDEXER); 215 synchronized (mSingleThreadedExecutor) { 216 mSingleThreadedExecutor.shutdown(); 217 } 218 boolean unused = mSingleThreadedExecutor.awaitTermination(30L, TimeUnit.SECONDS); 219 } 220 221 private class ContactsObserver extends ContentObserver { ContactsObserver()222 public ContactsObserver() { 223 super(/* handler= */ null); 224 } 225 226 @Override onChange(boolean selfChange, @NonNull Collection<Uri> uris, int flags)227 public void onChange(boolean selfChange, @NonNull Collection<Uri> uris, int flags) { 228 if (!selfChange) { 229 executeOnSingleThreadedExecutor( 230 ContactsIndexerUserInstance.this::handleDeltaUpdate); 231 } 232 } 233 } 234 235 /** 236 * Performs a one-time sync of CP2 contacts into AppSearch. 237 * 238 * <p>This handles the scenario where this contacts indexer instance has been started for the 239 * current device user for the first time or the background full update job is not scheduled. 240 * The full-update job which syncs all CP2 contacts is scheduled to run when the device is idle 241 * and its battery is not low. It can take several minutes or hours for these constraints to be 242 * met. Additionally, the delta-update job which runs on each CP2 change notification is 243 * designed to sync only the changed contacts because the user might be actively using the 244 * device at that time. 245 * 246 * <p>Schedules a one-off full update job to sync all CP2 contacts when the device is idle. Also 247 * syncs a configurable number of CP2 contacts into the AppSearch Person corpus so that it's 248 * nominally functional. 249 */ doCp2SyncFirstRun()250 private void doCp2SyncFirstRun() { 251 // If this is not the first run of contacts indexer (lastFullUpdateTimestampMillis is not 0) 252 // for the given user and a full update job is scheduled, this means that contacts indexer 253 // has been running recently and contacts should be up to date. The initial sync can be 254 // skipped in this case. 255 // If the job is not scheduled but lastFullUpdateTimestampMillis is not 0, the contacts 256 // indexer was disabled before. We need to reschedule the job and run a limited delta update 257 // to bring latest contact change in AppSearch right away, after it is re-enabled. 258 if (mSettings.getLastFullUpdateTimestampMillis() != 0 259 && IndexerMaintenanceService.isUpdateJobScheduled( 260 mContext, mContext.getUser(), CONTACTS_INDEXER)) { 261 return; 262 } 263 IndexerMaintenanceService.scheduleUpdateJob( 264 mContext, 265 mContext.getUser(), 266 CONTACTS_INDEXER, 267 /* periodic= */ false, 268 /* intervalMillis= */ -1); 269 // TODO(b/222126568): refactor doDeltaUpdateAsync() to return a future value of 270 // ContactsUpdateStats so that it can be checked and logged here, instead of the 271 // placeholder exceptionally() block that only logs to the console. 272 doDeltaUpdateAsync( 273 mContactsIndexerConfig.getContactsFirstRunIndexingLimit(), 274 new ContactsUpdateStats()) 275 .exceptionally( 276 t -> { 277 if (LogUtil.DEBUG) { 278 Log.d( 279 TAG, 280 "Failed to bootstrap Person corpus with CP2 contacts", 281 t); 282 } 283 return null; 284 }); 285 } 286 287 /** 288 * Performs a full sync of CP2 contacts to AppSearch builtin:Person corpus. 289 * 290 * @param signal Used to indicate if the full update task should be cancelled. 291 */ doFullUpdateAsync(@ullable CancellationSignal signal)292 public void doFullUpdateAsync(@Nullable CancellationSignal signal) { 293 executeOnSingleThreadedExecutor( 294 () -> { 295 ContactsUpdateStats updateStats = new ContactsUpdateStats(); 296 doFullUpdateInternalAsync(signal, updateStats); 297 IndexerMaintenanceService.scheduleUpdateJob( 298 mContext, 299 mContext.getUser(), 300 CONTACTS_INDEXER, 301 /* periodic= */ true, 302 mContactsIndexerConfig.getContactsFullUpdateIntervalMillis()); 303 }); 304 } 305 306 /** Dumps the internal state of this {@link ContactsIndexerUserInstance}. */ dump(@onNull PrintWriter pw, boolean verbose)307 public void dump(@NonNull PrintWriter pw, boolean verbose) { 308 // Those timestamps are not protected by any lock since in ContactsIndexerUserInstance 309 // we only have one thread to handle all the updates. It is possible we might run into 310 // race condition if there is an update running while those numbers are being printed. 311 // This is acceptable though for debug purpose, so still no lock here. 312 pw.println( 313 "last_full_update_timestamp_millis: " 314 + mSettings.getLastFullUpdateTimestampMillis()); 315 pw.println( 316 "last_delta_update_timestamp_millis: " 317 + mSettings.getLastDeltaUpdateTimestampMillis()); 318 pw.println( 319 "last_contact_update_timestamp_millis: " 320 + mSettings.getLastContactUpdateTimestampMillis()); 321 pw.println( 322 "last_contact_delete_timestamp_millis: " 323 + mSettings.getLastContactDeleteTimestampMillis()); 324 } 325 326 @VisibleForTesting doFullUpdateInternalAsync( @ullable CancellationSignal signal, @NonNull ContactsUpdateStats updateStats)327 CompletableFuture<Void> doFullUpdateInternalAsync( 328 @Nullable CancellationSignal signal, @NonNull ContactsUpdateStats updateStats) { 329 // TODO(b/203605504): handle cancellation signal to abort the job. 330 long currentTimeMillis = System.currentTimeMillis(); 331 updateStats.mUpdateType = ContactsUpdateStats.FULL_UPDATE; 332 updateStats.mUpdateAndDeleteStartTimeMillis = currentTimeMillis; 333 updateStats.mLastFullUpdateStartTimeMillis = mSettings.getLastFullUpdateTimestampMillis(); 334 updateStats.mLastDeltaUpdateStartTimeMillis = mSettings.getLastDeltaUpdateTimestampMillis(); 335 336 List<String> cp2ContactIds = new ArrayList<>(); 337 // Get a list of all contact IDs from CP2 338 updateStats.mLastContactUpdatedTimeMillis = 339 ContactsProviderUtil.getUpdatedContactIds( 340 mContext, 341 /* sinceFilter= */ 0, 342 mContactsIndexerConfig.getContactsFullUpdateLimit(), 343 cp2ContactIds, 344 updateStats); 345 updateStats.mPreviousLastContactUpdatedTimeMillis = 346 mSettings.getLastContactUpdateTimestampMillis(); 347 return mAppSearchHelper 348 .getAllContactIdsAsync() 349 .thenCompose( 350 appsearchContactIds -> { 351 // all_contacts_from_AppSearch - all_contacts_from_cp2 = 352 // contacts_needs_to_be_removed_from_AppSearch. 353 appsearchContactIds.removeAll(cp2ContactIds); 354 // Full update doesn't happen very often. In normal cases, it is 355 // scheduled to 356 // be run every 15-30 days. 357 // One-off full update can be scheduled if 358 // 1) during startup, full update has never been run. 359 // 2) or we get OUT_OF_SPACE from AppSearch. 360 // So print a message once in 15-30 days should be acceptable. 361 if (LogUtil.INFO) { 362 Log.i( 363 TAG, 364 "Performing a full sync (updated:" 365 + cp2ContactIds.size() 366 + ", deleted:" 367 + appsearchContactIds.size() 368 + ") of CP2 contacts in AppSearch"); 369 } 370 return mContactsIndexerImpl.updatePersonCorpusAsync( 371 /* wantedContactIds= */ cp2ContactIds, 372 /* unwantedContactIds= */ appsearchContactIds, 373 updateStats, 374 mContactsIndexerConfig.shouldKeepUpdatingOnError()); 375 }) 376 .handle( 377 (x, t) -> { 378 if (t != null) { 379 Log.w(TAG, "Failed to perform full update", t); 380 if (updateStats.mUpdateStatuses.isEmpty() 381 && updateStats.mDeleteStatuses.isEmpty()) { 382 // Somehow this error is not reflected in the stats, and 383 // unfortunately we don't know what part is wrong. Just add an 384 // error 385 // code for the update. 386 updateStats.mUpdateStatuses.add( 387 ContactsUpdateStats 388 .ERROR_CODE_CONTACTS_INDEXER_UNKNOWN_ERROR); 389 } 390 } 391 392 // Always persist the current timestamps for full update for both 393 // success and failure. Right now we are taking the best effort to keep 394 // CP2 and AppSearch in sync, without any retry in case of failure. We 395 // don't want an unexpected error, like a bad document, prevent the 396 // timestamps getting updated, which will make the indexer fetch a lot 397 // of contacts for EACH delta update. 398 // TODO(b/226078966) Also finding the update timestamps for last success 399 // is not trivial, and we should think more about how to do that 400 // correctly. 401 mSettings.setLastFullUpdateTimestampMillis(currentTimeMillis); 402 mSettings.setLastContactUpdateTimestampMillis(currentTimeMillis); 403 mSettings.setLastContactDeleteTimestampMillis(currentTimeMillis); 404 persistSettings(); 405 logStats(updateStats); 406 return null; 407 }); 408 } 409 410 /** 411 * Does the delta/instant update to sync the contacts from CP2 to AppSearch. 412 * 413 * <p>{@link #mDeltaUpdateScheduled} is being used to avoid scheduling any update BEFORE an 414 * active update finishes. 415 * 416 * <p>{@link #mSingleThreadedExecutor} is being used to make sure there is one and only one 417 * delta update can be scheduled and run. 418 */ 419 @VisibleForTesting handleDeltaUpdate()420 /*package*/ void handleDeltaUpdate() { 421 if (!ContentResolver.getCurrentSyncs().isEmpty()) { 422 // TODO(b/221905367): make sure that the delta update is scheduled as soon 423 // as the current sync is completed. 424 if (LogUtil.DEBUG) { 425 Log.v(TAG, "Deferring delta updates until the current sync is complete"); 426 } 427 return; 428 } 429 430 synchronized (mDeltaUpdateLock) { 431 // Record that a CP2 change notification has been received, and will be handled 432 // by the next delta update task. 433 mCp2ChangePending = true; 434 scheduleDeltaUpdateLocked(); 435 } 436 } 437 438 /** 439 * Schedule a delta update. No new delta update can be scheduled if there is one delta update 440 * already scheduled or currently being run. 441 * 442 * <p>ATTENTION!!! This function needs to be light weight since it is being called by CP2 with a 443 * lock. 444 */ 445 @GuardedBy("mDeltaUpdateLock") scheduleDeltaUpdateLocked()446 private void scheduleDeltaUpdateLocked() { 447 if (mDeltaUpdateScheduled) { 448 return; 449 } 450 mDeltaUpdateScheduled = true; 451 executeOnSingleThreadedExecutor( 452 () -> { 453 ContactsUpdateStats updateStats = new ContactsUpdateStats(); 454 // TODO(b/226489369): apply instant indexing limit on CP2 changes also? 455 // TODO(b/222126568): refactor doDeltaUpdateAsync() to return a future value of 456 // ContactsUpdateStats so that it can be checked and logged here, instead of 457 // the 458 // placeholder exceptionally() block that only logs to the console. 459 doDeltaUpdateAsync( 460 mContactsIndexerConfig.getContactsDeltaUpdateLimit(), 461 updateStats) 462 .exceptionally( 463 t -> { 464 if (LogUtil.DEBUG) { 465 Log.d(TAG, "Failed to index CP2 change", t); 466 } 467 return null; 468 }); 469 }); 470 } 471 472 /** 473 * Does the delta update. It also resets {@link 474 * ContactsIndexerUserInstance#mDeltaUpdateScheduled} to false. 475 */ 476 @VisibleForTesting doDeltaUpdateAsync( int indexingLimit, @NonNull ContactsUpdateStats updateStats)477 /*package*/ CompletableFuture<Void> doDeltaUpdateAsync( 478 int indexingLimit, @NonNull ContactsUpdateStats updateStats) { 479 synchronized (mDeltaUpdateLock) { 480 // Record that the CP2 change notification is being handled by this delta update task. 481 mCp2ChangePending = false; 482 } 483 484 long currentTimeMillis = System.currentTimeMillis(); 485 updateStats.mUpdateType = ContactsUpdateStats.DELTA_UPDATE; 486 updateStats.mUpdateAndDeleteStartTimeMillis = currentTimeMillis; 487 updateStats.mLastFullUpdateStartTimeMillis = mSettings.getLastFullUpdateTimestampMillis(); 488 updateStats.mLastDeltaUpdateStartTimeMillis = mSettings.getLastDeltaUpdateTimestampMillis(); 489 long lastContactUpdateTimestampMillis = mSettings.getLastContactUpdateTimestampMillis(); 490 long lastContactDeleteTimestampMillis = mSettings.getLastContactDeleteTimestampMillis(); 491 if (LogUtil.DEBUG) { 492 Log.d( 493 TAG, 494 "previous timestamps --" 495 + " lastContactUpdateTimestampMillis: " 496 + lastContactUpdateTimestampMillis 497 + " lastContactDeleteTimestampMillis: " 498 + lastContactDeleteTimestampMillis); 499 } 500 501 List<String> wantedIds = new ArrayList<>(); 502 List<String> unWantedIds = new ArrayList<>(); 503 long mostRecentContactUpdatedTimestampMillis = 504 ContactsProviderUtil.getUpdatedContactIds( 505 mContext, 506 lastContactUpdateTimestampMillis, 507 indexingLimit, 508 wantedIds, 509 updateStats); 510 long mostRecentContactDeletedTimestampMillis = 511 ContactsProviderUtil.getDeletedContactIds( 512 mContext, lastContactDeleteTimestampMillis, unWantedIds, updateStats); 513 updateStats.mLastContactUpdatedTimeMillis = mostRecentContactUpdatedTimestampMillis; 514 updateStats.mLastContactDeletedTimeMillis = mostRecentContactDeletedTimestampMillis; 515 516 // Update the person corpus in AppSearch based on the changed contact 517 // information we get from CP2. At this point mUpdateScheduled has been 518 // reset, so a new task is allowed to catch any new changes in CP2. 519 // TODO(b/203605504) for future improvement. Report errors here and persist the right 520 // timestamps for last successful deletion and update. This requires the ids from CP2 521 // are sorted in last_update_timestamp ascending order, and the code would become a 522 // little complicated. 523 return mContactsIndexerImpl 524 .updatePersonCorpusAsync( 525 wantedIds, 526 unWantedIds, 527 updateStats, 528 mContactsIndexerConfig.shouldKeepUpdatingOnError()) 529 .handle( 530 (x, t) -> { 531 try { 532 if (t != null) { 533 Log.w(TAG, "Failed to perform delta update", t); 534 if (updateStats.mUpdateStatuses.isEmpty() 535 && updateStats.mDeleteStatuses.isEmpty()) { 536 // Somehow this error is not reflected in the stats, and 537 // unfortunately we don't know which part is wrong. Just add 538 // an 539 // error code for the update. 540 updateStats.mUpdateStatuses.add( 541 ContactsUpdateStats 542 .ERROR_CODE_CONTACTS_INDEXER_UNKNOWN_ERROR); 543 } 544 } 545 // Persisting timestamping and logging, no matter if update succeeds 546 // or not. 547 if (LogUtil.DEBUG) { 548 Log.d( 549 TAG, 550 "updated timestamps --" 551 + " lastContactUpdateTimestampMillis: " 552 + mostRecentContactUpdatedTimestampMillis 553 + " lastContactDeleteTimestampMillis: " 554 + mostRecentContactDeletedTimestampMillis); 555 } 556 mSettings.setLastContactUpdateTimestampMillis( 557 mostRecentContactUpdatedTimestampMillis); 558 mSettings.setLastContactDeleteTimestampMillis( 559 mostRecentContactDeletedTimestampMillis); 560 mSettings.setLastDeltaUpdateTimestampMillis(currentTimeMillis); 561 persistSettings(); 562 logStats(updateStats); 563 if (updateStats.mUpdateStatuses.contains( 564 AppSearchResult.RESULT_OUT_OF_SPACE)) { 565 // Some indexing failed due to OUT_OF_SPACE from AppSearch. We 566 // can simply schedule a full update so we can trim the Person 567 // corpus in AppSearch to make some room for delta update. We 568 // need to monitor the failure count and reasons for indexing 569 // during full update to see if that limit (10,000) is too big 570 // right now, considering we are sharing this limit with any 571 // AppSearch clients, e.g. ShortcutManager, in the system 572 // server. 573 IndexerMaintenanceService.scheduleUpdateJob( 574 mContext, 575 mContext.getUser(), 576 CONTACTS_INDEXER, 577 /* periodic= */ false, 578 /* intervalMillis= */ -1); 579 } 580 581 return null; 582 } finally { 583 synchronized (mDeltaUpdateLock) { 584 // The current delta update is done. Reset the flag so new delta 585 // update can be scheduled and run. 586 mDeltaUpdateScheduled = false; 587 // If another CP2 change notifications were received while this 588 // delta 589 // update task was running, schedule it again. 590 if (mCp2ChangePending) { 591 scheduleDeltaUpdateLocked(); 592 } 593 } 594 } 595 }); 596 } 597 598 // Logs the stats to statsd. 599 @VisibleForTesting 600 void logStats(@NonNull ContactsUpdateStats updateStats) { 601 int totalUpdateLatency = 602 (int) (System.currentTimeMillis() - updateStats.mUpdateAndDeleteStartTimeMillis); 603 // Finalize status code for update and delete. 604 if (updateStats.mUpdateStatuses.isEmpty()) { 605 // SUCCESS if no error found. 606 updateStats.mUpdateStatuses.add(AppSearchResult.RESULT_OK); 607 } 608 if (updateStats.mDeleteStatuses.isEmpty()) { 609 // SUCCESS if no error found. 610 updateStats.mDeleteStatuses.add(AppSearchResult.RESULT_OK); 611 } 612 613 // Get the accurate count for failed cases. The current failed count doesn't include 614 // the contacts skipped due to failures in previous batches. Once a batch fails, all the 615 // following batches will be skipped. The contacts in those batches should be counted as 616 // failure as well. 617 updateStats.mContactsUpdateFailedCount = 618 updateStats.mTotalContactsToBeUpdated 619 - updateStats.mContactsUpdateSucceededCount 620 - updateStats.mContactsUpdateSkippedCount; 621 updateStats.mContactsDeleteFailedCount = 622 updateStats.mTotalContactsToBeDeleted - updateStats.mContactsDeleteSucceededCount; 623 624 int[] updateStatusArr = new int[updateStats.mUpdateStatuses.size()]; 625 int[] deleteStatusArr = new int[updateStats.mDeleteStatuses.size()]; 626 int updateIdx = 0; 627 int deleteIdx = 0; 628 for (int updateStatus : updateStats.mUpdateStatuses) { 629 updateStatusArr[updateIdx] = updateStatus; 630 ++updateIdx; 631 } 632 for (int deleteStatus : updateStats.mDeleteStatuses) { 633 deleteStatusArr[deleteIdx] = deleteStatus; 634 ++deleteIdx; 635 } 636 AppSearchStatsLog.write( 637 AppSearchStatsLog.CONTACTS_INDEXER_UPDATE_STATS_REPORTED, 638 updateStats.mUpdateType, 639 totalUpdateLatency, 640 updateStatusArr, 641 deleteStatusArr, 642 updateStats.mNewContactsToBeUpdated, 643 updateStats.mContactsUpdateSucceededCount, 644 updateStats.mContactsDeleteSucceededCount, 645 updateStats.mContactsUpdateSkippedCount, 646 updateStats.mContactsUpdateFailedCount, 647 updateStats.mContactsDeleteFailedCount, 648 updateStats.mContactsDeleteNotFoundCount, 649 updateStats.mUpdateAndDeleteStartTimeMillis, 650 updateStats.mLastFullUpdateStartTimeMillis, 651 updateStats.mLastDeltaUpdateStartTimeMillis, 652 updateStats.mLastContactUpdatedTimeMillis, 653 updateStats.mLastContactDeletedTimeMillis, 654 updateStats.mPreviousLastContactUpdatedTimeMillis); 655 } 656 657 /** 658 * Loads the persisted data from disk. 659 * 660 * <p>It doesn't throw here. If it fails to load file, ContactsIndexer would always use the 661 * timestamps persisted in the memory. 662 */ 663 private void loadSettingsAsync() { 664 executeOnSingleThreadedExecutor( 665 () -> { 666 boolean unused = mDataDir.mkdirs(); 667 try { 668 mSettings.load(); 669 } catch (IOException e) { 670 // Ignore file not found errors (bootstrap case) 671 if (!(e instanceof FileNotFoundException)) { 672 Log.w(TAG, "Failed to load settings from disk", e); 673 } 674 } 675 }); 676 } 677 678 private void persistSettings() { 679 try { 680 mSettings.persist(); 681 } catch (IOException e) { 682 Log.w(TAG, "Failed to save settings to disk", e); 683 } 684 } 685 686 /** 687 * Executes the given command on {@link #mSingleThreadedExecutor} if it is still alive. 688 * 689 * <p>If the {@link #mSingleThreadedExecutor} has been shutdown, this method doesn't execute the 690 * given command, and returns silently. Specifically, it does not throw {@link 691 * java.util.concurrent.RejectedExecutionException}. 692 * 693 * @param command the runnable task 694 */ 695 private void executeOnSingleThreadedExecutor(Runnable command) { 696 synchronized (mSingleThreadedExecutor) { 697 if (mSingleThreadedExecutor.isShutdown()) { 698 Log.w(TAG, "Executor is shutdown, not executing task"); 699 return; 700 } 701 mSingleThreadedExecutor.execute( 702 () -> { 703 try { 704 command.run(); 705 } catch (RuntimeException e) { 706 Slog.wtf( 707 TAG, 708 "ContactsIndexerUserInstance" 709 + ".executeOnSingleThreadedExecutor() failed ", 710 e); 711 } 712 }); 713 } 714 } 715 } 716