• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 static com.google.common.truth.Truth.assertThat;
22 
23 import static org.mockito.ArgumentMatchers.any;
24 import static org.mockito.Mockito.doReturn;
25 import static org.mockito.Mockito.mock;
26 import static org.mockito.Mockito.never;
27 import static org.mockito.Mockito.verify;
28 import static org.mockito.Mockito.verifyZeroInteractions;
29 
30 import android.annotation.NonNull;
31 import android.app.appsearch.AppSearchManager;
32 import android.app.appsearch.AppSearchResult;
33 import android.app.appsearch.AppSearchSessionShim;
34 import android.app.appsearch.GlobalSearchSessionShim;
35 import android.app.appsearch.SetSchemaRequest;
36 import android.app.appsearch.observer.DocumentChangeInfo;
37 import android.app.appsearch.observer.ObserverCallback;
38 import android.app.appsearch.observer.ObserverSpec;
39 import android.app.appsearch.observer.SchemaChangeInfo;
40 import android.app.appsearch.testutil.AppSearchSessionShimImpl;
41 import android.app.appsearch.testutil.GlobalSearchSessionShimImpl;
42 import android.app.appsearch.testutil.TestContactsIndexerConfig;
43 import android.app.job.JobInfo;
44 import android.app.job.JobScheduler;
45 import android.content.ContentResolver;
46 import android.content.ContentUris;
47 import android.content.ContentValues;
48 import android.os.CancellationSignal;
49 import android.os.PersistableBundle;
50 import android.provider.ContactsContract;
51 
52 import androidx.test.ext.junit.runners.AndroidJUnit4;
53 
54 import com.android.dx.mockito.inline.extended.ExtendedMockito;
55 import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
56 import com.android.modules.utils.testing.ExtendedMockitoRule;
57 import com.android.modules.utils.testing.StaticMockFixture;
58 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
59 import com.android.server.appsearch.indexer.IndexerMaintenanceService;
60 import com.android.server.appsearch.stats.AppSearchStatsLog;
61 
62 import org.junit.After;
63 import org.junit.Before;
64 import org.junit.Rule;
65 import org.junit.Test;
66 import org.junit.rules.TemporaryFolder;
67 import org.junit.runner.RunWith;
68 import org.mockito.ArgumentCaptor;
69 import org.mockito.Mockito;
70 
71 import java.io.File;
72 import java.util.List;
73 import java.util.concurrent.BlockingQueue;
74 import java.util.concurrent.CompletableFuture;
75 import java.util.concurrent.CompletionStage;
76 import java.util.concurrent.CountDownLatch;
77 import java.util.concurrent.ExecutorService;
78 import java.util.concurrent.Executors;
79 import java.util.concurrent.LinkedBlockingQueue;
80 import java.util.concurrent.ThreadPoolExecutor;
81 import java.util.concurrent.TimeUnit;
82 import java.util.concurrent.atomic.AtomicReference;
83 
84 @RunWith(AndroidJUnit4.class)
85 public class ContactsIndexerUserInstanceTest extends FakeContactsProviderTestBase {
86     @Rule
87     public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
88 
89     @Rule
90     public ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder()
91             .addStaticMockFixtures(TestMockFixture::new)
92             .build();
93 
94     private final ExecutorService mSingleThreadedExecutor = Executors.newSingleThreadExecutor();
95     private File mContactsDir;
96     private File mSettingsFile;
97     private ContactsIndexerUserInstance mInstance;
98     private ContactsUpdateStats mUpdateStats;
99     private ContactsIndexerConfig mConfigForTest = new TestContactsIndexerConfig();
100 
101     @Override
102     @Before
setUp()103     public void setUp() throws Exception {
104         super.setUp();
105 
106         // Setup the file path to the persisted data
107         mContactsDir = new File(mTemporaryFolder.newFolder(), "appsearch/contacts");
108         mSettingsFile = new File(mContactsDir, ContactsIndexerSettings.SETTINGS_FILE_NAME);
109         mInstance = ContactsIndexerUserInstance.createInstance(mContext, mContactsDir,
110                 mConfigForTest, mSingleThreadedExecutor);
111         mUpdateStats = new ContactsUpdateStats();
112     }
113 
114     @Override
115     @After
tearDown()116     public void tearDown() throws Exception {
117         // Wipe the data in AppSearchHelper.DATABASE_NAME.
118         AppSearchManager.SearchContext searchContext =
119                 new AppSearchManager.SearchContext.Builder(AppSearchHelper.DATABASE_NAME).build();
120         AppSearchSessionShim db = AppSearchSessionShimImpl.createSearchSessionAsync(
121                 searchContext).get();
122         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
123                 .setForceOverride(true).build();
124         db.setSchemaAsync(setSchemaRequest).get();
125         super.tearDown();
126     }
127 
128     @Test
testHandleMultipleNotifications_onlyOneDeltaUpdateCanBeScheduledAndRun()129     public void testHandleMultipleNotifications_onlyOneDeltaUpdateCanBeScheduledAndRun()
130             throws Exception {
131         try {
132             long dataQueryDelayMs = 5000;
133             mFakeContactsProvider.setDataQueryDelayMs(dataQueryDelayMs);
134             BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
135             ThreadPoolExecutor singleThreadedExecutor =
136                     new ThreadPoolExecutor(/*corePoolSize=*/1, /*maximumPoolSize=*/
137                             1, /*KeepAliveTime=*/ 0L, TimeUnit.MILLISECONDS, queue);
138             ContactsIndexerUserInstance instance = ContactsIndexerUserInstance.createInstance(
139                     mContext, mContactsDir,
140                     mConfigForTest, singleThreadedExecutor);
141 
142             int numOfNotifications = 20;
143             for (int i = 0; i < numOfNotifications / 2; ++i) {
144                 int docCount = 2;
145                 // Insert contacts to trigger delta update.
146                 ContentResolver resolver = mContext.getContentResolver();
147                 ContentValues dummyValues = new ContentValues();
148                 for (int j = 0; j < docCount; j++) {
149                     resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
150                 }
151                 instance.handleDeltaUpdate();
152             }
153             // Sleep here so the active delta update can be run for some time. While
154             // notifications come before and afterwards.
155             Thread.sleep(1500);
156             long totalTaskAfterFirstDeltaUpdate = singleThreadedExecutor.getTaskCount();
157             for (int i = 0; i < numOfNotifications / 2; ++i) {
158                 int docCount = 2;
159                 // Insert contacts to trigger delta update.
160                 ContentResolver resolver = mContext.getContentResolver();
161                 ContentValues dummyValues = new ContentValues();
162                 for (int j = 0; j < docCount; j++) {
163                     resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
164                 }
165                 instance.handleDeltaUpdate();
166             }
167 
168             // The total task count will be increased if there is another delta update scheduled.
169             assertThat(singleThreadedExecutor.getTaskCount()).isEqualTo(
170                     totalTaskAfterFirstDeltaUpdate);
171         } finally {
172             mFakeContactsProvider.setDataQueryDelayMs(0);
173         }
174     }
175 
176     @Test
testHandleNotificationDuringUpdate_oneAdditionalUpdateWillBeRun()177     public void testHandleNotificationDuringUpdate_oneAdditionalUpdateWillBeRun()
178             throws Exception {
179         try {
180             long dataQueryDelayMs = 5000;
181             mFakeContactsProvider.setDataQueryDelayMs(dataQueryDelayMs);
182             BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
183             ThreadPoolExecutor singleThreadedExecutor =
184                     new ThreadPoolExecutor(/*corePoolSize=*/1, /*maximumPoolSize=*/
185                             1, /*KeepAliveTime=*/ 0L, TimeUnit.MILLISECONDS, queue);
186             ContactsIndexerUserInstance instance = ContactsIndexerUserInstance.createInstance(
187                     mContext, mContactsDir,
188                     mConfigForTest, singleThreadedExecutor);
189             int docCount = 10;
190             // Insert contacts to trigger delta update.
191             ContentResolver resolver = mContext.getContentResolver();
192             ContentValues dummyValues = new ContentValues();
193 
194             for (int j = 0; j < docCount; j++) {
195                 resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
196             }
197             instance.handleDeltaUpdate();
198             // Sleep here so the active delta update can be run for some time to make sure
199             // notification come during an update.
200             Thread.sleep(1500);
201             long totalTaskAfterFirstDeltaUpdate = singleThreadedExecutor.getTaskCount();
202             // Insert contacts to trigger delta update.
203             for (int j = 0; j < docCount; j++) {
204                 resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
205             }
206             instance.handleDeltaUpdate();
207 
208             //The 2nd update won't be scheduled right away.
209             assertThat(singleThreadedExecutor.getTaskCount()).isEqualTo(
210                     totalTaskAfterFirstDeltaUpdate);
211 
212             // To make sure the 1st update has been finished.
213             Thread.sleep(dataQueryDelayMs);
214 
215             // This means additional task has been run by the 1st delta update to handle
216             // the change notification.
217             assertThat(singleThreadedExecutor.getActiveCount()).isEqualTo(1);
218         } finally {
219             mFakeContactsProvider.setDataQueryDelayMs(0);
220         }
221     }
222 
223     @Test
testCreateInstance_dataDirectoryCreatedAsynchronously()224     public void testCreateInstance_dataDirectoryCreatedAsynchronously() throws Exception {
225         File dataDir = new File(mTemporaryFolder.newFolder(), "contacts");
226         boolean isDataDirectoryCreatedSynchronously = mSingleThreadedExecutor.submit(() -> {
227             ContactsIndexerUserInstance unused =
228                     ContactsIndexerUserInstance.createInstance(mContext, dataDir, mConfigForTest,
229                             mSingleThreadedExecutor);
230             // Data directory shouldn't have been created synchronously in createInstance()
231             return dataDir.exists();
232         }).get();
233         assertThat(isDataDirectoryCreatedSynchronously).isFalse();
234         boolean isDataDirectoryCreatedAsynchronously = mSingleThreadedExecutor.submit(
235                 dataDir::exists).get();
236         assertThat(isDataDirectoryCreatedAsynchronously).isTrue();
237     }
238 
239     @Test
testStart_initialRun_schedulesFullUpdateJob()240     public void testStart_initialRun_schedulesFullUpdateJob() throws Exception {
241         JobScheduler mockJobScheduler = mock(JobScheduler.class);
242         mContext.setJobScheduler(mockJobScheduler);
243         ContactsIndexerUserInstance instance = ContactsIndexerUserInstance.createInstance(
244                 mContext,
245                 mContactsDir, mConfigForTest, mSingleThreadedExecutor);
246 
247         int docCount = 100;
248         CountDownLatch latch = new CountDownLatch(docCount);
249         GlobalSearchSessionShim shim =
250                 GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
251         ObserverCallback callback = new ObserverCallback() {
252             @Override
253             public void onSchemaChanged(SchemaChangeInfo changeInfo) {
254                 // Do nothing
255             }
256 
257             @Override
258             public void onDocumentChanged(DocumentChangeInfo changeInfo) {
259                 for (int i = 0; i < changeInfo.getChangedDocumentIds().size(); i++) {
260                     latch.countDown();
261                 }
262             }
263         };
264         shim.registerObserverCallback(mContext.getPackageName(),
265                 new ObserverSpec.Builder().addFilterSchemas("builtin:Person").build(),
266                 mSingleThreadedExecutor,
267                 callback);
268         // Insert contacts to trigger delta update.
269         ContentResolver resolver = mContext.getContentResolver();
270         ContentValues dummyValues = new ContentValues();
271         for (int i = 0; i < docCount; i++) {
272             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
273         }
274 
275         try {
276             instance.startAsync();
277 
278             // Wait for all async tasks to complete
279             latch.await(30L, TimeUnit.SECONDS);
280 
281             ArgumentCaptor<JobInfo> jobInfoArgumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
282             verify(mockJobScheduler).schedule(jobInfoArgumentCaptor.capture());
283             JobInfo fullUpdateJob = jobInfoArgumentCaptor.getValue();
284             assertThat(fullUpdateJob.isRequireBatteryNotLow()).isTrue();
285             assertThat(fullUpdateJob.isRequireDeviceIdle()).isTrue();
286             assertThat(fullUpdateJob.isPersisted()).isTrue();
287             assertThat(fullUpdateJob.isPeriodic()).isFalse();
288         } finally {
289             instance.shutdown();
290         }
291     }
292 
293     @Test
testStart_subsequentRunWithNoScheduledJob_schedulesFullUpdateJob()294     public void testStart_subsequentRunWithNoScheduledJob_schedulesFullUpdateJob()
295             throws Exception {
296         // Trigger an initial full update.
297         executeAndWaitForCompletion(
298                 mInstance.doFullUpdateInternalAsync(new CancellationSignal(), mUpdateStats),
299                 mSingleThreadedExecutor);
300 
301         // By default mockJobScheduler.getPendingJob() would return null. This simulates the
302         // scenario where the scheduled full update job after the initial run is cancelled
303         // due to some reason.
304         JobScheduler mockJobScheduler = mock(JobScheduler.class);
305         mContext.setJobScheduler(mockJobScheduler);
306         ContactsIndexerUserInstance instance = ContactsIndexerUserInstance.createInstance(
307                 mContext, mContactsDir, mConfigForTest, mSingleThreadedExecutor);
308 
309         int docCount = 100;
310         CountDownLatch latch = new CountDownLatch(docCount);
311         GlobalSearchSessionShim shim =
312                 GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
313         ObserverCallback callback = new ObserverCallback() {
314             @Override
315             public void onSchemaChanged(SchemaChangeInfo changeInfo) {
316                 // Do nothing
317             }
318 
319             @Override
320             public void onDocumentChanged(DocumentChangeInfo changeInfo) {
321                 for (int i = 0; i < changeInfo.getChangedDocumentIds().size(); i++) {
322                     latch.countDown();
323                 }
324             }
325         };
326         shim.registerObserverCallback(mContext.getPackageName(),
327                 new ObserverSpec.Builder().addFilterSchemas("builtin:Person").build(),
328                 mSingleThreadedExecutor,
329                 callback);
330         // Insert contacts to trigger delta update.
331         ContentResolver resolver = mContext.getContentResolver();
332         ContentValues dummyValues = new ContentValues();
333         for (int i = 0; i < docCount; i++) {
334             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
335         }
336 
337         try {
338             instance.startAsync();
339 
340             // Wait for all async tasks to complete
341             latch.await(30L, TimeUnit.SECONDS);
342 
343             ArgumentCaptor<JobInfo> jobInfoArgumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
344             verify(mockJobScheduler).schedule(jobInfoArgumentCaptor.capture());
345             JobInfo fullUpdateJob = jobInfoArgumentCaptor.getValue();
346             assertThat(fullUpdateJob.isRequireBatteryNotLow()).isTrue();
347             assertThat(fullUpdateJob.isRequireDeviceIdle()).isTrue();
348             assertThat(fullUpdateJob.isPersisted()).isTrue();
349             assertThat(fullUpdateJob.isPeriodic()).isFalse();
350         } finally {
351             instance.shutdown();
352         }
353     }
354 
355     @Test
testStart_subsequentRunWithScheduledJob_doesNotScheduleFullUpdateJob()356     public void testStart_subsequentRunWithScheduledJob_doesNotScheduleFullUpdateJob()
357             throws Exception {
358         // Trigger an initial full update.
359         executeAndWaitForCompletion(
360                 mInstance.doFullUpdateInternalAsync(new CancellationSignal(), mUpdateStats),
361                 mSingleThreadedExecutor);
362 
363         JobScheduler mockJobScheduler = mock(JobScheduler.class);
364         JobInfo mockJobInfo = mock(JobInfo.class);
365         // getPendingJob() should return a non-null value to simulate the scenario where a
366         // background job is already scheduled.
367         doReturn(mockJobInfo)
368                 .when(mockJobScheduler)
369                 .getPendingJob(
370                         ContactsIndexerMaintenanceConfig.MIN_CONTACTS_INDEXER_JOB_ID
371                                 + mContext.getUser().getIdentifier());
372         mContext.setJobScheduler(mockJobScheduler);
373         ContactsIndexerUserInstance instance = ContactsIndexerUserInstance.createInstance(
374                 mContext, mContactsDir, mConfigForTest, mSingleThreadedExecutor);
375 
376         int docCount = 100;
377         CountDownLatch latch = new CountDownLatch(docCount);
378         GlobalSearchSessionShim shim =
379                 GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
380         ObserverCallback callback = new ObserverCallback() {
381             @Override
382             public void onSchemaChanged(SchemaChangeInfo changeInfo) {
383                 // Do nothing
384             }
385 
386             @Override
387             public void onDocumentChanged(DocumentChangeInfo changeInfo) {
388                 for (int i = 0; i < changeInfo.getChangedDocumentIds().size(); i++) {
389                     latch.countDown();
390                 }
391             }
392         };
393         shim.registerObserverCallback(mContext.getPackageName(),
394                 new ObserverSpec.Builder().addFilterSchemas("builtin:Person").build(),
395                 mSingleThreadedExecutor,
396                 callback);
397         // Insert contacts to trigger delta update.
398         ContentResolver resolver = mContext.getContentResolver();
399         ContentValues dummyValues = new ContentValues();
400         for (int i = 0; i < docCount; i++) {
401             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
402         }
403 
404         try {
405             instance.startAsync();
406 
407             // Wait for all async tasks to complete
408             latch.await(30L, TimeUnit.SECONDS);
409 
410             verify(mockJobScheduler, never()).schedule(any());
411         } finally {
412             instance.shutdown();
413         }
414     }
415 
416     @Test
testFullUpdate()417     public void testFullUpdate() throws Exception {
418         ContentResolver resolver = mContext.getContentResolver();
419         ContentValues dummyValues = new ContentValues();
420         for (int i = 0; i < 500; i++) {
421             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
422         }
423 
424         executeAndWaitForCompletion(
425                 mInstance.doFullUpdateInternalAsync(new CancellationSignal(), mUpdateStats),
426                 mSingleThreadedExecutor);
427 
428         AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext,
429                 mSingleThreadedExecutor, mConfigForTest);
430         List<String> contactIds = searchHelper.getAllContactIdsAsync().get();
431         assertThat(contactIds.size()).isEqualTo(500);
432     }
433 
434     @Test
testFullUpdate_setsPreviousLastContactUpdatedTimestamp()435     public void testFullUpdate_setsPreviousLastContactUpdatedTimestamp() throws Exception {
436         long timeBeforeDeltaChangeNotification = System.currentTimeMillis();
437         // Insert contact
438         ContentResolver resolver = mContext.getContentResolver();
439         ContentValues dummyValues = new ContentValues();
440         resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
441 
442         executeAndWaitForCompletion(
443                 mInstance.doDeltaUpdateAsync(/*indexingLimit=*/ -1, mUpdateStats),
444                 mSingleThreadedExecutor);
445 
446         // check that delta update set the last contact updated timestamp (but not the previous one)
447         assertThat(mUpdateStats.mLastContactUpdatedTimeMillis).isAtLeast(
448                 timeBeforeDeltaChangeNotification);
449         assertThat(mUpdateStats.mPreviousLastContactUpdatedTimeMillis).isEqualTo(0);
450 
451         // Insert another contact
452         resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
453 
454         ContactsUpdateStats updateStats = new ContactsUpdateStats();
455         executeAndWaitForCompletion(
456                 mInstance.doFullUpdateInternalAsync(new CancellationSignal(), updateStats),
457                 mSingleThreadedExecutor);
458 
459         // check that full update set the previous last contact updated timestamp
460         assertThat(updateStats.mLastContactUpdatedTimeMillis).isAtLeast(
461                 mUpdateStats.mLastContactUpdatedTimeMillis);
462         assertThat(updateStats.mPreviousLastContactUpdatedTimeMillis).isEqualTo(
463                 mUpdateStats.mLastContactUpdatedTimeMillis);
464     }
465 
466     @Test
testDeltaUpdate_insertedContacts()467     public void testDeltaUpdate_insertedContacts() throws Exception {
468         long timeBeforeDeltaChangeNotification = System.currentTimeMillis();
469         // Insert contacts to trigger delta update.
470         ContentResolver resolver = mContext.getContentResolver();
471         ContentValues dummyValues = new ContentValues();
472         for (int i = 0; i < 250; i++) {
473             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
474         }
475 
476         executeAndWaitForCompletion(mInstance.doDeltaUpdateAsync(/*indexingLimit=*/ -1,
477                         mUpdateStats),
478                 mSingleThreadedExecutor);
479 
480         AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext,
481                 mSingleThreadedExecutor, mConfigForTest);
482         List<String> contactIds = searchHelper.getAllContactIdsAsync().get();
483         assertThat(contactIds.size()).isEqualTo(250);
484 
485         PersistableBundle settingsBundle = ContactsIndexerSettings.readBundle(mSettingsFile);
486         assertThat(settingsBundle.getLong(ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY))
487                 .isAtLeast(timeBeforeDeltaChangeNotification);
488         assertThat(
489                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_UPDATE_TIMESTAMP_KEY))
490                 .isAtLeast(timeBeforeDeltaChangeNotification);
491         // check stats
492         assertThat(mUpdateStats.mUpdateType).isEqualTo(ContactsUpdateStats.DELTA_UPDATE);
493         assertThat(mUpdateStats.mUpdateStatuses).hasSize(1);
494         assertThat(mUpdateStats.mUpdateStatuses).containsExactly(AppSearchResult.RESULT_OK);
495         assertThat(mUpdateStats.mDeleteStatuses).hasSize(1);
496         assertThat(mUpdateStats.mDeleteStatuses).containsExactly(AppSearchResult.RESULT_OK);
497         assertThat(mUpdateStats.mContactsUpdateFailedCount).isEqualTo(0);
498         assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(0);
499         assertThat(mUpdateStats.mContactsDeleteNotFoundCount).isEqualTo(0);
500         assertThat(mUpdateStats.mNewContactsToBeUpdated).isEqualTo(250);
501         assertThat(mUpdateStats.mContactsUpdateSkippedCount).isEqualTo(0);
502         assertThat(mUpdateStats.mTotalContactsToBeUpdated).isEqualTo(250);
503         assertThat(mUpdateStats.mContactsUpdateSucceededCount).isEqualTo(250);
504         assertThat(mUpdateStats.mTotalContactsToBeDeleted).isEqualTo(0);
505         assertThat(mUpdateStats.mContactsDeleteSucceededCount).isEqualTo(0);
506         // check timestamps
507         assertThat(mUpdateStats.mUpdateAndDeleteStartTimeMillis).isEqualTo(settingsBundle.getLong(
508                 ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY));
509         assertThat(mUpdateStats.mLastDeltaUpdateStartTimeMillis).isLessThan(settingsBundle.getLong(
510                 ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY));
511         assertThat(mUpdateStats.mLastContactUpdatedTimeMillis).isEqualTo(
512                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_UPDATE_TIMESTAMP_KEY));
513         assertThat(mUpdateStats.mLastContactDeletedTimeMillis).isEqualTo(
514                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_DELETE_TIMESTAMP_KEY));
515     }
516 
517     @Test
testDeltaUpdateWithLimit_fewerContactsIndexed()518     public void testDeltaUpdateWithLimit_fewerContactsIndexed() throws Exception {
519         // Insert contacts to trigger delta update.
520         ContentResolver resolver = mContext.getContentResolver();
521         ContentValues dummyValues = new ContentValues();
522         for (int i = 0; i < 250; i++) {
523             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
524         }
525 
526         executeAndWaitForCompletion(mInstance.doDeltaUpdateAsync(/*indexingLimit=*/ 100,
527                         mUpdateStats),
528                 mSingleThreadedExecutor);
529 
530         AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext,
531                 mSingleThreadedExecutor, mConfigForTest);
532         List<String> contactIds = searchHelper.getAllContactIdsAsync().get();
533         assertThat(contactIds.size()).isEqualTo(100);
534     }
535 
536     @Test
testDeltaUpdate_deletedContacts()537     public void testDeltaUpdate_deletedContacts() throws Exception {
538         long timeBeforeDeltaChangeNotification = System.currentTimeMillis();
539         // Insert contacts to trigger delta update.
540         ContentResolver resolver = mContext.getContentResolver();
541         ContentValues dummyValues = new ContentValues();
542         for (int i = 0; i < 10; i++) {
543             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
544         }
545 
546         executeAndWaitForCompletion(mInstance.doDeltaUpdateAsync(/*indexingLimit=*/ -1,
547                         mUpdateStats),
548                 mSingleThreadedExecutor);
549 
550         // Delete a few contacts to trigger delta update.
551         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 2),
552                 /*extras=*/ null);
553         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 3),
554                 /*extras=*/ null);
555         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 5),
556                 /*extras=*/ null);
557         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 7),
558                 /*extras=*/ null);
559 
560         executeAndWaitForCompletion(mInstance.doDeltaUpdateAsync(/*indexingLimit=*/ -1,
561                         mUpdateStats),
562                 mSingleThreadedExecutor);
563 
564         AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext,
565                 mSingleThreadedExecutor, mConfigForTest);
566         List<String> contactIds = searchHelper.getAllContactIdsAsync().get();
567         assertThat(contactIds.size()).isEqualTo(6);
568         assertThat(contactIds).containsNoneOf("2", "3", "5", "7");
569 
570         PersistableBundle settingsBundle = ContactsIndexerSettings.readBundle(mSettingsFile);
571         assertThat(settingsBundle.getLong(ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY))
572                 .isAtLeast(timeBeforeDeltaChangeNotification);
573         assertThat(
574                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_DELETE_TIMESTAMP_KEY))
575                 .isAtLeast(timeBeforeDeltaChangeNotification);
576         // check stats
577         assertThat(mUpdateStats.mUpdateType).isEqualTo(ContactsUpdateStats.DELTA_UPDATE);
578         assertThat(mUpdateStats.mUpdateStatuses).hasSize(1);
579         assertThat(mUpdateStats.mUpdateStatuses).containsExactly(AppSearchResult.RESULT_OK);
580         assertThat(mUpdateStats.mDeleteStatuses).hasSize(1);
581         assertThat(mUpdateStats.mDeleteStatuses).containsExactly(AppSearchResult.RESULT_OK);
582         assertThat(mUpdateStats.mContactsUpdateFailedCount).isEqualTo(0);
583         assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(0);
584         assertThat(mUpdateStats.mContactsDeleteNotFoundCount).isEqualTo(0);
585         assertThat(mUpdateStats.mNewContactsToBeUpdated).isEqualTo(10);
586         assertThat(mUpdateStats.mContactsUpdateSkippedCount).isEqualTo(0);
587         assertThat(mUpdateStats.mTotalContactsToBeUpdated).isEqualTo(10);
588         assertThat(mUpdateStats.mContactsUpdateSucceededCount).isEqualTo(10);
589         assertThat(mUpdateStats.mTotalContactsToBeDeleted).isEqualTo(4);
590         assertThat(mUpdateStats.mContactsDeleteSucceededCount).isEqualTo(4);
591         // check timestamps
592         assertThat(mUpdateStats.mUpdateAndDeleteStartTimeMillis).isEqualTo(settingsBundle.getLong(
593                 ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY));
594         assertThat(mUpdateStats.mLastDeltaUpdateStartTimeMillis).isLessThan(settingsBundle.getLong(
595                 ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY));
596         assertThat(mUpdateStats.mLastContactUpdatedTimeMillis).isEqualTo(
597                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_UPDATE_TIMESTAMP_KEY));
598         assertThat(mUpdateStats.mLastContactDeletedTimeMillis).isEqualTo(
599                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_DELETE_TIMESTAMP_KEY));
600     }
601 
602     @Test
testDeltaUpdate_insertedAndDeletedContacts()603     public void testDeltaUpdate_insertedAndDeletedContacts() throws Exception {
604         long timeBeforeDeltaChangeNotification = System.currentTimeMillis();
605         // Insert contacts to trigger delta update.
606         ContentResolver resolver = mContext.getContentResolver();
607         ContentValues dummyValues = new ContentValues();
608         for (int i = 0; i < 10; i++) {
609             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
610         }
611 
612         // Delete a few contacts to trigger delta update.
613         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 2),
614                 /*extras=*/ null);
615         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 3),
616                 /*extras=*/ null);
617         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 5),
618                 /*extras=*/ null);
619         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 7),
620                 /*extras=*/ null);
621 
622         mUpdateStats.clear();
623         executeAndWaitForCompletion(mInstance.doDeltaUpdateAsync(/*indexingLimit=*/ -1,
624                         mUpdateStats),
625                 mSingleThreadedExecutor);
626 
627         AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext,
628                 mSingleThreadedExecutor, mConfigForTest);
629         List<String> contactIds = searchHelper.getAllContactIdsAsync().get();
630         assertThat(contactIds.size()).isEqualTo(6);
631         assertThat(contactIds).containsNoneOf("2", "3", "5", "7");
632 
633         PersistableBundle settingsBundle = ContactsIndexerSettings.readBundle(mSettingsFile);
634         assertThat(settingsBundle.getLong(ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY))
635                 .isAtLeast(timeBeforeDeltaChangeNotification);
636         assertThat(
637                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_UPDATE_TIMESTAMP_KEY))
638                 .isAtLeast(timeBeforeDeltaChangeNotification);
639         assertThat(
640                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_DELETE_TIMESTAMP_KEY))
641                 .isAtLeast(timeBeforeDeltaChangeNotification);
642         // check stats
643         assertThat(mUpdateStats.mUpdateType).isEqualTo(ContactsUpdateStats.DELTA_UPDATE);
644         assertThat(mUpdateStats.mUpdateStatuses).hasSize(1);
645         assertThat(mUpdateStats.mUpdateStatuses).containsExactly(AppSearchResult.RESULT_OK);
646         assertThat(mUpdateStats.mDeleteStatuses).hasSize(1);
647         assertThat(mUpdateStats.mDeleteStatuses).containsExactly(AppSearchResult.RESULT_NOT_FOUND);
648         assertThat(mUpdateStats.mContactsUpdateFailedCount).isEqualTo(0);
649         // 4 contacts deleted in CP2, but we don't have those in AppSearch. So we will get
650         // NOT_FOUND.
651         assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(4);
652         assertThat(mUpdateStats.mContactsDeleteNotFoundCount).isEqualTo(4);
653         assertThat(mUpdateStats.mNewContactsToBeUpdated).isEqualTo(6);
654         assertThat(mUpdateStats.mContactsUpdateSkippedCount).isEqualTo(0);
655         assertThat(mUpdateStats.mTotalContactsToBeUpdated).isEqualTo(6);
656         assertThat(mUpdateStats.mContactsUpdateSucceededCount).isEqualTo(6);
657         assertThat(mUpdateStats.mTotalContactsToBeDeleted).isEqualTo(4);
658         assertThat(mUpdateStats.mContactsDeleteSucceededCount).isEqualTo(0);
659         // check timestamps
660         assertThat(mUpdateStats.mUpdateAndDeleteStartTimeMillis).isEqualTo(settingsBundle.getLong(
661                 ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY));
662         assertThat(mUpdateStats.mLastDeltaUpdateStartTimeMillis).isLessThan(settingsBundle.getLong(
663                 ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY));
664         assertThat(mUpdateStats.mLastContactUpdatedTimeMillis).isEqualTo(
665                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_UPDATE_TIMESTAMP_KEY));
666         assertThat(mUpdateStats.mLastContactDeletedTimeMillis).isEqualTo(
667                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_DELETE_TIMESTAMP_KEY));
668     }
669 
670     @Test
testDeltaUpdate_insertedAndDeletedContacts_withDeletionSucceed()671     public void testDeltaUpdate_insertedAndDeletedContacts_withDeletionSucceed() throws Exception {
672         ContentResolver resolver = mContext.getContentResolver();
673         ContentValues dummyValues = new ContentValues();
674         // Index 10 documents before testing.
675         for (int i = 0; i < 10; i++) {
676             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
677         }
678         executeAndWaitForCompletion(
679                 mInstance.doFullUpdateInternalAsync(new CancellationSignal(), mUpdateStats),
680                 mSingleThreadedExecutor);
681 
682         long timeBeforeDeltaChangeNotification = System.currentTimeMillis();
683         // Insert additional 5 contacts to trigger delta update.
684         for (int i = 0; i < 5; i++) {
685             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
686         }
687         // Delete a few contacts to trigger delta update.
688         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 2),
689                 /*extras=*/ null);
690         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 3),
691                 /*extras=*/ null);
692         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 5),
693                 /*extras=*/ null);
694         resolver.delete(ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 7),
695                 /*extras=*/ null);
696 
697         mUpdateStats.clear();
698         executeAndWaitForCompletion(mInstance.doDeltaUpdateAsync(/*indexingLimit=*/ -1,
699                         mUpdateStats),
700                 mSingleThreadedExecutor);
701 
702         AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext,
703                 mSingleThreadedExecutor, mConfigForTest);
704         List<String> contactIds = searchHelper.getAllContactIdsAsync().get();
705         assertThat(contactIds.size()).isEqualTo(11);
706         assertThat(contactIds).containsNoneOf("2", "3", "5", "7");
707 
708         PersistableBundle settingsBundle = ContactsIndexerSettings.readBundle(mSettingsFile);
709         assertThat(settingsBundle.getLong(ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY))
710                 .isAtLeast(timeBeforeDeltaChangeNotification);
711         assertThat(
712                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_UPDATE_TIMESTAMP_KEY))
713                 .isAtLeast(timeBeforeDeltaChangeNotification);
714         assertThat(
715                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_DELETE_TIMESTAMP_KEY))
716                 .isAtLeast(timeBeforeDeltaChangeNotification);
717         // check stats
718         assertThat(mUpdateStats.mUpdateType).isEqualTo(ContactsUpdateStats.DELTA_UPDATE);
719         assertThat(mUpdateStats.mUpdateStatuses).hasSize(1);
720         assertThat(mUpdateStats.mUpdateStatuses).containsExactly(AppSearchResult.RESULT_OK);
721         assertThat(mUpdateStats.mDeleteStatuses).hasSize(1);
722         assertThat(mUpdateStats.mDeleteStatuses).containsExactly(AppSearchResult.RESULT_OK);
723         assertThat(mUpdateStats.mContactsUpdateFailedCount).isEqualTo(0);
724         // NOT_FOUND does not count as error.
725         assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(0);
726         assertThat(mUpdateStats.mContactsDeleteNotFoundCount).isEqualTo(0);
727         assertThat(mUpdateStats.mNewContactsToBeUpdated).isEqualTo(5);
728         assertThat(mUpdateStats.mContactsUpdateSkippedCount).isEqualTo(0);
729         assertThat(mUpdateStats.mTotalContactsToBeUpdated).isEqualTo(5);
730         assertThat(mUpdateStats.mContactsUpdateSucceededCount).isEqualTo(5);
731         assertThat(mUpdateStats.mTotalContactsToBeDeleted).isEqualTo(4);
732         assertThat(mUpdateStats.mContactsDeleteSucceededCount).isEqualTo(4);
733         // check timestamps
734         assertThat(mUpdateStats.mUpdateAndDeleteStartTimeMillis).isEqualTo(settingsBundle.getLong(
735                 ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY));
736         assertThat(mUpdateStats.mLastDeltaUpdateStartTimeMillis).isLessThan(settingsBundle.getLong(
737                 ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY));
738         assertThat(mUpdateStats.mLastContactUpdatedTimeMillis).isEqualTo(
739                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_UPDATE_TIMESTAMP_KEY));
740         assertThat(mUpdateStats.mLastContactDeletedTimeMillis).isEqualTo(
741                 settingsBundle.getLong(ContactsIndexerSettings.LAST_CONTACT_DELETE_TIMESTAMP_KEY));
742     }
743 
744     @Test
testDeltaUpdate_outOfSpaceError_fullUpdateScheduled()745     public void testDeltaUpdate_outOfSpaceError_fullUpdateScheduled() throws Exception {
746         // This tests whether a full update job will be run to prune the person corpus when
747         // AppSearch reaches its max document limit. Since there are issues with obtaining the
748         // permissions to change the device config for max document limit, and we don't want to
749         // index 10000+ documents in this test, we simulate the out of space error by manually
750         // adding it to update stats beforehand.
751 
752         // Cancel any existing jobs.
753         IndexerMaintenanceService.cancelUpdateJobIfScheduled(
754                 mContext, mContext.getUser(), CONTACTS_INDEXER);
755 
756         JobScheduler mockJobScheduler = mock(JobScheduler.class);
757         mContext.setJobScheduler(mockJobScheduler);
758 
759         // manually add out of space error
760         mUpdateStats.mUpdateStatuses.add(AppSearchResult.RESULT_OUT_OF_SPACE);
761 
762         executeAndWaitForCompletion(
763                 mInstance.doDeltaUpdateAsync(ContactsProviderUtil.UPDATE_LIMIT_NONE,
764                         mUpdateStats),
765                 mSingleThreadedExecutor);
766 
767         // Verify the full update job is scheduled due to out_of_space.
768         verify(mockJobScheduler).schedule(any());
769     }
770 
771     @Test
testDeltaUpdate_notTriggered_afterCompatibleSchemaChange()772     public void testDeltaUpdate_notTriggered_afterCompatibleSchemaChange() throws Exception {
773         long timeAtBeginning = System.currentTimeMillis();
774 
775         // Configure the timestamps to non-zero on disk.
776         PersistableBundle settingsBundle = new PersistableBundle();
777         settingsBundle.putLong(ContactsIndexerSettings.LAST_FULL_UPDATE_TIMESTAMP_KEY,
778                 timeAtBeginning);
779         settingsBundle.putLong(ContactsIndexerSettings.LAST_CONTACT_UPDATE_TIMESTAMP_KEY,
780                 timeAtBeginning);
781         settingsBundle.putLong(ContactsIndexerSettings.LAST_CONTACT_DELETE_TIMESTAMP_KEY,
782                 timeAtBeginning);
783         mSettingsFile.getParentFile().mkdirs();
784         mSettingsFile.createNewFile();
785         ContactsIndexerSettings.writeBundle(mSettingsFile, settingsBundle);
786         // Preset a compatible schema.
787         AppSearchManager.SearchContext searchContext =
788                 new AppSearchManager.SearchContext.Builder(AppSearchHelper.DATABASE_NAME).build();
789         AppSearchSessionShim db = AppSearchSessionShimImpl.createSearchSessionAsync(
790                 searchContext).get();
791         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
792                 .addSchemas(TestUtils.CONTACT_POINT_SCHEMA_WITH_APP_IDS_OPTIONAL,
793                         Person.getSchema(mConfigForTest))
794                 .setForceOverride(true).build();
795         db.setSchemaAsync(setSchemaRequest).get();
796 
797         // Since the current schema is compatible, this won't trigger any delta update and
798         // schedule a full update job.
799         JobScheduler mockJobScheduler = mock(JobScheduler.class);
800         mContext.setJobScheduler(mockJobScheduler);
801         mInstance = ContactsIndexerUserInstance.createInstance(mContext, mContactsDir,
802                 mConfigForTest, mSingleThreadedExecutor);
803         try {
804             mInstance.startAsync();
805             verifyZeroInteractions(mockJobScheduler);
806         } finally {
807             mInstance.shutdown();
808         }
809     }
810 
811     @Test
testDeltaUpdate_triggered_afterIncompatibleSchemaChange()812     public void testDeltaUpdate_triggered_afterIncompatibleSchemaChange() throws Exception {
813         long timeBeforeDeltaChangeNotification = System.currentTimeMillis();
814 
815         // Configure the timestamps to non-zero on disk.
816         PersistableBundle settingsBundle = new PersistableBundle();
817         settingsBundle.putLong(ContactsIndexerSettings.LAST_FULL_UPDATE_TIMESTAMP_KEY,
818                 timeBeforeDeltaChangeNotification);
819         settingsBundle.putLong(ContactsIndexerSettings.LAST_CONTACT_UPDATE_TIMESTAMP_KEY,
820                 timeBeforeDeltaChangeNotification);
821         settingsBundle.putLong(ContactsIndexerSettings.LAST_CONTACT_DELETE_TIMESTAMP_KEY,
822                 timeBeforeDeltaChangeNotification);
823         mSettingsFile.getParentFile().mkdirs();
824         mSettingsFile.createNewFile();
825         ContactsIndexerSettings.writeBundle(mSettingsFile, settingsBundle);
826         // Insert contacts
827         int docCount = 250;
828         ContentResolver resolver = mContext.getContentResolver();
829         ContentValues dummyValues = new ContentValues();
830         for (int i = 0; i < docCount; i++) {
831             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
832         }
833         // Preset an incompatible schema.
834         AppSearchManager.SearchContext searchContext =
835                 new AppSearchManager.SearchContext.Builder(AppSearchHelper.DATABASE_NAME).build();
836         AppSearchSessionShim db = AppSearchSessionShimImpl.createSearchSessionAsync(
837                 searchContext).get();
838         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
839                 .addSchemas(TestUtils.CONTACT_POINT_SCHEMA_WITH_LABEL_REPEATED,
840                         Person.getSchema(mConfigForTest))
841                 .setForceOverride(true).build();
842         db.setSchemaAsync(setSchemaRequest).get();
843         // Setup a latch
844         CountDownLatch latch = new CountDownLatch(docCount);
845         GlobalSearchSessionShim shim =
846                 GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
847         ObserverCallback callback = new ObserverCallback() {
848             @Override
849             public void onSchemaChanged(SchemaChangeInfo changeInfo) {
850                 // Do nothing
851             }
852 
853             @Override
854             public void onDocumentChanged(DocumentChangeInfo changeInfo) {
855                 for (int i = 0; i < changeInfo.getChangedDocumentIds().size(); i++) {
856                     latch.countDown();
857                 }
858             }
859         };
860         shim.registerObserverCallback(mContext.getPackageName(),
861                 new ObserverSpec.Builder().addFilterSchemas("builtin:Person").build(),
862                 mSingleThreadedExecutor,
863                 callback);
864 
865         // Since the current schema is incompatible, this will trigger two setSchemas, and run do
866         // doCp2SyncFirstRun again.
867         JobScheduler mockJobScheduler = mock(JobScheduler.class);
868         mContext.setJobScheduler(mockJobScheduler);
869         mInstance = ContactsIndexerUserInstance.createInstance(mContext, mContactsDir,
870                 mConfigForTest, mSingleThreadedExecutor);
871         try {
872             mInstance.startAsync();
873             latch.await(30L, TimeUnit.SECONDS);
874             verify(mockJobScheduler).schedule(any());
875         } finally {
876             mInstance.shutdown();
877         }
878     }
879 
880     @Test
testLogStats_succeedsWhenMoreUpdateStatusCodesThanDeleteStatusCodes()881     public void testLogStats_succeedsWhenMoreUpdateStatusCodesThanDeleteStatusCodes() {
882         // This test exists since there was a typo/bug where we logged the update status codes
883         // a second time instead of the delete status codes. This could also throw
884         // ArrayIndexOutOfBoundsException if there were more update status codes than delete status
885         // codes since the allocated array for delete status codes would be too small to store the
886         // update status codes.
887         ContactsUpdateStats updateStats = new ContactsUpdateStats();
888         updateStats.mUpdateStatuses.add(AppSearchResult.RESULT_UNKNOWN_ERROR);
889         updateStats.mUpdateStatuses.add(AppSearchResult.RESULT_OUT_OF_SPACE);
890         updateStats.mDeleteStatuses.add(AppSearchResult.RESULT_INTERNAL_ERROR);
891         mInstance.logStats(updateStats);
892 
893         ArgumentCaptor<int[]> updateStatusArr = ArgumentCaptor.forClass(int[].class);
894         ArgumentCaptor<int[]> deleteStatusArr = ArgumentCaptor.forClass(int[].class);
895 
896         ExtendedMockito.verify(() -> AppSearchStatsLog.write(
897                 Mockito.eq(AppSearchStatsLog.CONTACTS_INDEXER_UPDATE_STATS_REPORTED),
898                 Mockito.anyInt(),
899                 Mockito.anyInt(),
900                 updateStatusArr.capture(),
901                 deleteStatusArr.capture(),
902                 Mockito.anyInt(),
903                 Mockito.anyInt(),
904                 Mockito.anyInt(),
905                 Mockito.anyInt(),
906                 Mockito.anyInt(),
907                 Mockito.anyInt(),
908                 Mockito.anyInt(),
909                 Mockito.anyLong(),
910                 Mockito.anyLong(),
911                 Mockito.anyLong(),
912                 Mockito.anyLong(),
913                 Mockito.anyLong(),
914                 Mockito.anyLong()));
915 
916         assertThat(updateStatusArr.getValue()).asList().containsExactly(
917                 AppSearchResult.RESULT_UNKNOWN_ERROR, AppSearchResult.RESULT_OUT_OF_SPACE);
918         assertThat(deleteStatusArr.getValue()).asList().containsExactly(
919                 AppSearchResult.RESULT_INTERNAL_ERROR);
920     }
921 
922     @Test
testConcurrentUpdates_updatesDoNotInterfereWithEachOther()923     public void testConcurrentUpdates_updatesDoNotInterfereWithEachOther() throws Exception {
924         // Generally, two delta updates cannot occur simultaneously, but it is possible for a full
925         // update and delta update to run at the same time. Both updates use the same
926         // ContactsIndexerImpl to index contacts, and previously, ContactsIndexerImpl would keep
927         // a single ContactsBatcher for all updates. This could lead to updates taking contacts away
928         // from each other to index and would mess up the metrics/counts for succeeded/skipped
929         // contacts. This has been fixed by using local ContactsBatchers instead.
930         long timeBeforeDeltaChangeNotification = System.currentTimeMillis();
931         // Insert contacts to trigger delta update.
932         ContentResolver resolver = mContext.getContentResolver();
933         ContentValues dummyValues = new ContentValues();
934         for (int i = 0; i < 250; i++) {
935             resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues);
936         }
937 
938         mSingleThreadedExecutor.submit(
939                 () -> mInstance.doDeltaUpdateAsync(/*indexingLimit=*/ -1, mUpdateStats));
940 
941         ContactsUpdateStats updateStats = new ContactsUpdateStats();
942         executeAndWaitForCompletion(
943                 mInstance.doFullUpdateInternalAsync(new CancellationSignal(), updateStats),
944                 mSingleThreadedExecutor);
945 
946         AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext,
947                 mSingleThreadedExecutor, mConfigForTest);
948         List<String> contactIds = searchHelper.getAllContactIdsAsync().get();
949         assertThat(contactIds.size()).isEqualTo(250);
950 
951         PersistableBundle settingsBundle = ContactsIndexerSettings.readBundle(mSettingsFile);
952         assertThat(settingsBundle.getLong(ContactsIndexerSettings.LAST_DELTA_UPDATE_TIMESTAMP_KEY))
953                 .isAtLeast(timeBeforeDeltaChangeNotification);
954 
955         // check stats
956         assertThat(mUpdateStats.mUpdateType).isEqualTo(ContactsUpdateStats.DELTA_UPDATE);
957         assertThat(mUpdateStats.mUpdateStatuses).hasSize(1);
958         assertThat(mUpdateStats.mUpdateStatuses).containsExactly(AppSearchResult.RESULT_OK);
959         assertThat(mUpdateStats.mDeleteStatuses).hasSize(1);
960         assertThat(mUpdateStats.mDeleteStatuses).containsExactly(AppSearchResult.RESULT_OK);
961         assertThat(mUpdateStats.mContactsUpdateFailedCount).isEqualTo(0);
962         assertThat(mUpdateStats.mContactsDeleteFailedCount).isEqualTo(0);
963         assertThat(mUpdateStats.mContactsDeleteNotFoundCount).isEqualTo(0);
964         assertThat(mUpdateStats.mTotalContactsToBeUpdated).isEqualTo(250);
965         assertThat(mUpdateStats.mContactsUpdateSucceededCount
966                 + mUpdateStats.mContactsUpdateSkippedCount).isEqualTo(250);
967         assertThat(mUpdateStats.mTotalContactsToBeDeleted).isEqualTo(0);
968         assertThat(mUpdateStats.mContactsDeleteSucceededCount).isEqualTo(0);
969 
970         // check stats
971         assertThat(updateStats.mUpdateType).isEqualTo(ContactsUpdateStats.FULL_UPDATE);
972         assertThat(updateStats.mUpdateStatuses).hasSize(1);
973         assertThat(updateStats.mUpdateStatuses).containsExactly(AppSearchResult.RESULT_OK);
974         assertThat(updateStats.mDeleteStatuses).hasSize(1);
975         assertThat(updateStats.mDeleteStatuses).containsExactly(AppSearchResult.RESULT_OK);
976         assertThat(updateStats.mContactsUpdateFailedCount).isEqualTo(0);
977         // NOT_FOUND does not count as error.
978         assertThat(updateStats.mContactsDeleteFailedCount).isEqualTo(0);
979         assertThat(updateStats.mTotalContactsToBeUpdated).isEqualTo(250);
980         assertThat(updateStats.mContactsUpdateSucceededCount
981                 + updateStats.mContactsUpdateSkippedCount).isEqualTo(250);
982         assertThat(updateStats.mTotalContactsToBeDeleted).isEqualTo(0);
983         assertThat(updateStats.mContactsDeleteSucceededCount).isEqualTo(0);
984     }
985 
986     /**
987      * Executes given {@link CompletionStage} on the {@code executor} and waits for its completion.
988      *
989      * <p>There are 2 steps in this implementation. The first step is to execute the stage on the
990      * executor, and wait for its execution. The second step is to wait for the completion of the
991      * stage itself.
992      */
executeAndWaitForCompletion(CompletionStage<T> stage, ExecutorService executor)993     private <T> T executeAndWaitForCompletion(CompletionStage<T> stage, ExecutorService executor)
994             throws Exception {
995         AtomicReference<CompletableFuture<T>> future = new AtomicReference<>(
996                 CompletableFuture.completedFuture(null));
997         executor.submit(() -> {
998             // Chain the given stage inside the runnable task so that it executes on the executor.
999             CompletableFuture<T> chainedFuture = future.get().thenCompose(x -> stage);
1000             future.set(chainedFuture);
1001         }).get();
1002         // Wait for the task to complete on the executor, and wait for the stage to complete also.
1003         return future.get().get();
1004     }
1005 
1006     private static class TestMockFixture implements StaticMockFixture {
1007         @Override
setUpMockedClasses( @onNull StaticMockitoSessionBuilder sessionBuilder)1008         public StaticMockitoSessionBuilder setUpMockedClasses(
1009                 @NonNull StaticMockitoSessionBuilder sessionBuilder) {
1010             sessionBuilder.spyStatic(AppSearchStatsLog.class);
1011             return sessionBuilder;
1012         }
1013 
1014         @Override
setUpMockBehaviors()1015         public void setUpMockBehaviors() {
1016         }
1017 
1018         @Override
tearDown()1019         public void tearDown() {
1020         }
1021     }
1022 }
1023