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