• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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