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.google.common.truth.Truth.assertThat; 20 21 import static org.junit.Assert.assertThrows; 22 import static org.mockito.ArgumentMatchers.any; 23 import static org.mockito.Mockito.doCallRealMethod; 24 import static org.mockito.Mockito.spy; 25 import static org.mockito.Mockito.times; 26 import static org.mockito.Mockito.verify; 27 28 import android.annotation.NonNull; 29 import android.app.appsearch.AppSearchManager; 30 import android.app.appsearch.AppSearchSessionShim; 31 import android.app.appsearch.SetSchemaRequest; 32 import android.app.appsearch.testutil.AppSearchSessionShimImpl; 33 import android.content.ContentResolver; 34 import android.content.ContentValues; 35 import android.content.Context; 36 import android.content.ContextWrapper; 37 import android.provider.ContactsContract; 38 import android.test.mock.MockContentResolver; 39 import android.util.Pair; 40 41 import androidx.test.core.app.ApplicationProvider; 42 43 import com.android.server.appsearch.contactsindexer.ContactsIndexerImpl.ContactsBatcher; 44 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person; 45 46 import com.google.common.collect.ImmutableList; 47 48 import org.junit.After; 49 import org.junit.Before; 50 import org.junit.Test; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 import java.util.Objects; 55 import java.util.concurrent.CompletableFuture; 56 import java.util.concurrent.ExecutionException; 57 import java.util.stream.Collectors; 58 59 public class ContactsIndexerImplTest extends FakeContactsProviderTestBase { 60 // TODO(b/203605504) we could just use AppSearchHelper. 61 private FakeAppSearchHelper mAppSearchHelper; 62 private ContactsUpdateStats mUpdateStats; 63 64 @Override 65 @Before setUp()66 public void setUp() throws Exception { 67 super.setUp(); 68 mAppSearchHelper = new FakeAppSearchHelper(mContext); 69 mUpdateStats = new ContactsUpdateStats(); 70 } 71 72 @Override 73 @After tearDown()74 public void tearDown() throws Exception { 75 // Wipe the data in AppSearchHelper.DATABASE_NAME. 76 AppSearchManager.SearchContext searchContext = 77 new AppSearchManager.SearchContext.Builder(AppSearchHelper.DATABASE_NAME).build(); 78 AppSearchSessionShim db = AppSearchSessionShimImpl.createSearchSessionAsync( 79 searchContext).get(); 80 SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder() 81 .setForceOverride(true).build(); 82 db.setSchemaAsync(setSchemaRequest).get(); 83 super.tearDown(); 84 } 85 86 /** 87 * Helper method to run a delta update in the test. 88 * 89 * <p> Get is called on the futures to make this helper method synchronous. 90 * 91 * @param lastUpdatedTimestamp used as the "since" filter for updating the contacts. 92 * @param lastDeletedTimestamp used as the "since" filter for deleting the contacts. 93 * @return new (lastUpdatedTimestamp, lastDeletedTimestamp) pair after the update and deletion. 94 */ runDeltaUpdateOnContactsIndexerImpl( @onNull ContactsIndexerImpl indexerImpl, long lastUpdatedTimestamp, long lastDeletedTimestamp, @NonNull ContactsUpdateStats updateStats)95 private Pair<Long, Long> runDeltaUpdateOnContactsIndexerImpl( 96 @NonNull ContactsIndexerImpl indexerImpl, 97 long lastUpdatedTimestamp, 98 long lastDeletedTimestamp, 99 @NonNull ContactsUpdateStats updateStats) 100 throws ExecutionException, InterruptedException { 101 Objects.requireNonNull(indexerImpl); 102 Objects.requireNonNull(updateStats); 103 List<String> wantedContactIds = new ArrayList<>(); 104 List<String> unWantedContactIds = new ArrayList<>(); 105 106 lastUpdatedTimestamp = ContactsProviderUtil.getUpdatedContactIds(mContext, 107 lastUpdatedTimestamp, ContactsProviderUtil.UPDATE_LIMIT_NONE, 108 wantedContactIds, /*stats=*/ null); 109 lastDeletedTimestamp = ContactsProviderUtil.getDeletedContactIds(mContext, 110 lastDeletedTimestamp, unWantedContactIds, /*stats=*/ null); 111 indexerImpl.updatePersonCorpusAsync(wantedContactIds, unWantedContactIds, 112 updateStats, /*shouldKeepUpdatingOnError=*/ true).get(); 113 114 return new Pair<>(lastUpdatedTimestamp, lastDeletedTimestamp); 115 } 116 117 @Test testBatcher_noFlushBeforeReachingLimit()118 public void testBatcher_noFlushBeforeReachingLimit() throws Exception { 119 int batchSize = 5; 120 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize, 121 /*shouldKeepUpdatingOnError=*/ true); 122 123 for (int i = 0; i < batchSize - 1; ++i) { 124 batcher.add(new PersonBuilderHelper(/*id=*/ String.valueOf(i), 125 new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/ 126 String.valueOf(i))).setCreationTimestampMillis(0), 127 mUpdateStats); 128 } 129 batcher.getCompositeFuture().get(); 130 131 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 132 } 133 134 @Test testBatcher_autoFlush()135 public void testBatcher_autoFlush() throws Exception { 136 int batchSize = 5; 137 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize, 138 /*shouldKeepUpdatingOnError=*/ true); 139 140 for (int i = 0; i < batchSize; ++i) { 141 batcher.add( 142 new PersonBuilderHelper( 143 /*id=*/ String.valueOf(i), 144 new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/ 145 String.valueOf(i)) 146 ).setCreationTimestampMillis(0), mUpdateStats); 147 } 148 batcher.getCompositeFuture().get(); 149 150 assertThat(mAppSearchHelper.mIndexedContacts).hasSize(batchSize); 151 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 152 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 153 } 154 155 @Test testBatcher_contactFingerprintSame_notIndexed()156 public void testBatcher_contactFingerprintSame_notIndexed() throws Exception { 157 int batchSize = 2; 158 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize, 159 /*shouldKeepUpdatingOnError=*/ true); 160 PersonBuilderHelper builderHelper1 = new PersonBuilderHelper("id1", 161 new Person.Builder("namespace", "id1", "name1") 162 .setGivenName("given1") 163 ).setCreationTimestampMillis(0); 164 PersonBuilderHelper builderHelper2 = new PersonBuilderHelper("id2", 165 new Person.Builder("namespace", "id2", "name2") 166 .setGivenName("given2") 167 ).setCreationTimestampMillis(0); 168 mAppSearchHelper.setExistingContacts(ImmutableList.of(builderHelper1.buildPerson(), 169 builderHelper2.buildPerson())); 170 171 // Try to add the same contacts 172 batcher.add(builderHelper1, mUpdateStats); 173 batcher.add(builderHelper2, mUpdateStats); 174 batcher.getCompositeFuture().get(); 175 176 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 177 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 178 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 179 } 180 181 @Test testBatcher_contactFingerprintDifferent_notIndexedButBatched()182 public void testBatcher_contactFingerprintDifferent_notIndexedButBatched() throws Exception { 183 int batchSize = 2; 184 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize, 185 /*shouldKeepUpdatingOnError=*/ true); 186 PersonBuilderHelper builderHelper1 = new PersonBuilderHelper("id1", 187 new Person.Builder("namespace", "id1", "name1") 188 .setGivenName("given1") 189 ).setCreationTimestampMillis(0); 190 PersonBuilderHelper builderHelper2 = new PersonBuilderHelper("id2", 191 new Person.Builder("namespace", "id2", "name2") 192 .setGivenName("given2") 193 ).setCreationTimestampMillis(0); 194 mAppSearchHelper.setExistingContacts( 195 ImmutableList.of(builderHelper1.buildPerson(), builderHelper2.buildPerson())); 196 197 PersonBuilderHelper sameAsContact1 = builderHelper1; 198 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 199 // the test does depend on framework-appsearch.impl. 200 PersonBuilderHelper notSameAsContact2 = new PersonBuilderHelper("id2", 201 new Person.Builder("namespace", "id2", "name2").setGivenName( 202 "given2diff") 203 ).setCreationTimestampMillis(0); 204 batcher.add(sameAsContact1, mUpdateStats); 205 batcher.add(notSameAsContact2, mUpdateStats); 206 batcher.getCompositeFuture().get(); 207 208 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 209 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 210 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1); 211 } 212 213 @Test testBatcher_contactFingerprintDifferent_Indexed()214 public void testBatcher_contactFingerprintDifferent_Indexed() throws Exception { 215 int batchSize = 2; 216 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize, 217 /*shouldKeepUpdatingOnError=*/ true); 218 PersonBuilderHelper contact1 = new PersonBuilderHelper("id1", 219 new Person.Builder("namespace", "id1", "name1") 220 .setGivenName("given1") 221 ).setCreationTimestampMillis(0); 222 PersonBuilderHelper contact2 = new PersonBuilderHelper("id2", 223 new Person.Builder("namespace", "id2", "name2") 224 .setGivenName("given2") 225 ).setCreationTimestampMillis(0); 226 PersonBuilderHelper contact3 = new PersonBuilderHelper("id3", 227 new Person.Builder("namespace", "id3", "name3") 228 .setGivenName("given3") 229 ).setCreationTimestampMillis(0); 230 mAppSearchHelper.setExistingContacts( 231 ImmutableList.of(contact1.buildPerson(), contact2.buildPerson(), 232 contact3.buildPerson())); 233 234 PersonBuilderHelper sameAsContact1 = contact1; 235 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 236 // the test does depend on framework-appsearch.impl. 237 PersonBuilderHelper notSameAsContact2 = new PersonBuilderHelper("id2", 238 new Person.Builder("namespace", "id2", "name2").setGivenName( 239 "given2diff") 240 ).setCreationTimestampMillis(0); 241 PersonBuilderHelper notSameAsContact3 = new PersonBuilderHelper("id3", 242 new Person.Builder("namespace", "id3", "name3").setGivenName( 243 "given3diff") 244 ).setCreationTimestampMillis(0); 245 batcher.add(sameAsContact1, mUpdateStats); 246 batcher.add(notSameAsContact2, mUpdateStats); 247 batcher.add(notSameAsContact3, mUpdateStats); 248 batcher.getCompositeFuture().get(); 249 250 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 251 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(1); 252 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1); 253 254 batcher.flushAsync(mUpdateStats).get(); 255 256 assertThat(mAppSearchHelper.mIndexedContacts).containsExactly( 257 notSameAsContact2.buildPerson(), 258 notSameAsContact3.buildPerson()); 259 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 260 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 261 } 262 263 @Test testBatcher_contactFingerprintDifferent_IndexedWithOriginalCreationTimestamp()264 public void testBatcher_contactFingerprintDifferent_IndexedWithOriginalCreationTimestamp() 265 throws Exception { 266 int batchSize = 2; 267 long originalTs = System.currentTimeMillis(); 268 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize, 269 /*shouldKeepUpdatingOnError=*/ true); 270 PersonBuilderHelper contact1 = new PersonBuilderHelper("id1", 271 new Person.Builder("namespace", "id1", "name1") 272 .setGivenName("given1") 273 ).setCreationTimestampMillis(originalTs); 274 PersonBuilderHelper contact2 = new PersonBuilderHelper("id2", 275 new Person.Builder("namespace", "id2", "name2") 276 .setGivenName("given2") 277 ).setCreationTimestampMillis(originalTs); 278 PersonBuilderHelper contact3 = new PersonBuilderHelper("id3", 279 new Person.Builder("namespace", "id3", "name3") 280 .setGivenName("given3") 281 ).setCreationTimestampMillis(originalTs); 282 mAppSearchHelper.setExistingContacts( 283 ImmutableList.of(contact1.buildPerson(), contact2.buildPerson(), 284 contact3.buildPerson())); 285 long updatedTs1 = originalTs + 1; 286 long updatedTs2 = originalTs + 2; 287 long updatedTs3 = originalTs + 3; 288 PersonBuilderHelper sameAsContact1 = new PersonBuilderHelper("id1", 289 new Person.Builder("namespace", "id1", "name1") 290 .setGivenName("given1") 291 ).setCreationTimestampMillis(updatedTs1); 292 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 293 // the test does depend on framework-appsearch.impl. 294 PersonBuilderHelper notSameAsContact2 = new PersonBuilderHelper("id2", 295 new Person.Builder("namespace", "id2", "name2").setGivenName( 296 "given2diff") 297 ).setCreationTimestampMillis(updatedTs2); 298 PersonBuilderHelper notSameAsContact3 = new PersonBuilderHelper("id3", 299 new Person.Builder("namespace", "id3", "name3").setGivenName( 300 "given3diff") 301 ).setCreationTimestampMillis(updatedTs3); 302 303 assertThat(sameAsContact1.buildPerson().getCreationTimestampMillis()).isEqualTo(updatedTs1); 304 assertThat(notSameAsContact2.buildPerson().getCreationTimestampMillis()).isEqualTo( 305 updatedTs2); 306 assertThat(notSameAsContact3.buildPerson().getCreationTimestampMillis()).isEqualTo( 307 updatedTs3); 308 309 batcher.add(sameAsContact1, mUpdateStats); 310 batcher.add(notSameAsContact2, mUpdateStats); 311 batcher.add(notSameAsContact3, mUpdateStats); 312 batcher.flushAsync(mUpdateStats).get(); 313 314 assertThat(mAppSearchHelper.mIndexedContacts).hasSize(2); 315 assertThat(mAppSearchHelper.mExistingContacts.get( 316 "id1").getGivenName()).isEqualTo("given1"); 317 assertThat(mAppSearchHelper.mExistingContacts.get( 318 "id2").getGivenName()).isEqualTo("given2diff"); 319 assertThat(mAppSearchHelper.mExistingContacts.get( 320 "id3").getGivenName()).isEqualTo("given3diff"); 321 // But the timestamps remain same. 322 assertThat(mAppSearchHelper.mExistingContacts.get( 323 "id1").getCreationTimestampMillis()).isEqualTo(originalTs); 324 assertThat(mAppSearchHelper.mExistingContacts.get( 325 "id2").getCreationTimestampMillis()).isEqualTo(originalTs); 326 assertThat(mAppSearchHelper.mExistingContacts.get( 327 "id3").getCreationTimestampMillis()).isEqualTo(originalTs); 328 } 329 330 @Test testBatcher_contactNew_notIndexedButBatched()331 public void testBatcher_contactNew_notIndexedButBatched() throws Exception { 332 int batchSize = 2; 333 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize, 334 /*shouldKeepUpdatingOnError=*/ true); 335 PersonBuilderHelper contact1 = new PersonBuilderHelper("id1", 336 new Person.Builder("namespace", "id1", "name1") 337 .setGivenName("given1") 338 ).setCreationTimestampMillis(0); 339 mAppSearchHelper.setExistingContacts(ImmutableList.of(contact1.buildPerson())); 340 341 PersonBuilderHelper sameAsContact1 = contact1; 342 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 343 // the test does depend on framework-appsearch.impl. 344 PersonBuilderHelper newContact = new PersonBuilderHelper("id2", 345 new Person.Builder("namespace", "id2", "name2").setGivenName( 346 "given2diff") 347 ).setCreationTimestampMillis(0); 348 batcher.add(sameAsContact1, mUpdateStats); 349 batcher.add(newContact, mUpdateStats); 350 batcher.getCompositeFuture().get(); 351 352 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 353 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 354 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1); 355 } 356 357 @Test testBatcher_contactNew_indexed()358 public void testBatcher_contactNew_indexed() throws Exception { 359 int batchSize = 2; 360 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize, 361 /*shouldKeepUpdatingOnError=*/ true); 362 PersonBuilderHelper contact1 = new PersonBuilderHelper("id1", 363 new Person.Builder("namespace", "id1", "name1") 364 .setGivenName("given1")).setCreationTimestampMillis(0); 365 mAppSearchHelper.setExistingContacts(ImmutableList.of(contact1.buildPerson())); 366 367 PersonBuilderHelper sameAsContact1 = contact1; 368 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 369 // the test does depend on framework-appsearch.impl. 370 PersonBuilderHelper newContact1 = new PersonBuilderHelper("id2", 371 new Person.Builder("namespace", "id2", "name2").setGivenName( 372 "given2diff") 373 ).setCreationTimestampMillis(0); 374 PersonBuilderHelper newContact2 = new PersonBuilderHelper("id3", 375 new Person.Builder("namespace", "id3", "name3").setGivenName( 376 "given3diff") 377 ).setCreationTimestampMillis(0); 378 batcher.add(sameAsContact1, mUpdateStats); 379 batcher.add(newContact1, mUpdateStats); 380 batcher.add(newContact2, mUpdateStats); 381 batcher.getCompositeFuture().get(); 382 383 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 384 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(1); 385 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1); 386 387 batcher.flushAsync(mUpdateStats).get(); 388 389 assertThat(mAppSearchHelper.mIndexedContacts).containsExactly(newContact1.buildPerson(), 390 newContact2.buildPerson()); 391 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 392 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 393 } 394 395 @Test testBatcher_batchedContactClearedAfterFlush()396 public void testBatcher_batchedContactClearedAfterFlush() throws Exception { 397 int batchSize = 5; 398 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize, 399 /*shouldKeepUpdatingOnError=*/ true); 400 401 // First batch 402 for (int i = 0; i < batchSize; ++i) { 403 batcher.add(new PersonBuilderHelper(/*id=*/ String.valueOf(i), 404 new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/ 405 String.valueOf(i)) 406 ).setCreationTimestampMillis(0), mUpdateStats); 407 } 408 batcher.getCompositeFuture().get(); 409 410 assertThat(mAppSearchHelper.mIndexedContacts).hasSize(batchSize); 411 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 412 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 413 414 415 mAppSearchHelper.mIndexedContacts.clear(); 416 // Second batch. Make sure the first batch has been cleared. 417 for (int i = 0; i < batchSize; ++i) { 418 batcher.add(new PersonBuilderHelper(/*id=*/ String.valueOf(i), 419 new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/ 420 String.valueOf(i)) 421 // Different from previous ones to bypass the fingerprinting. 422 .addNote("note") 423 ).setCreationTimestampMillis(0), mUpdateStats); 424 } 425 batcher.getCompositeFuture().get(); 426 427 assertThat(mAppSearchHelper.mIndexedContacts).hasSize(batchSize); 428 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 429 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 430 } 431 432 @Test testContactsIndexerImpl_batchRemoveContacts_largerThanBatchSize()433 public void testContactsIndexerImpl_batchRemoveContacts_largerThanBatchSize() throws Exception { 434 ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(mContext, 435 mAppSearchHelper); 436 int totalNum = ContactsIndexerImpl.NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH + 1; 437 List<String> removedIds = new ArrayList<>(totalNum); 438 for (int i = 0; i < totalNum; ++i) { 439 removedIds.add(String.valueOf(i)); 440 } 441 442 contactsIndexerImpl.batchRemoveContactsAsync(removedIds, 443 mUpdateStats, /*shouldKeepUpdatingOnError=*/ true).get(); 444 445 assertThat(mAppSearchHelper.mRemovedIds).hasSize(removedIds.size()); 446 assertThat(mAppSearchHelper.mRemovedIds).isEqualTo(removedIds); 447 } 448 449 @Test testContactsIndexerImpl_batchRemoveContacts_smallerThanBatchSize()450 public void testContactsIndexerImpl_batchRemoveContacts_smallerThanBatchSize() 451 throws Exception { 452 ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(mContext, 453 mAppSearchHelper); 454 int totalNum = ContactsIndexerImpl.NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH - 1; 455 List<String> removedIds = new ArrayList<>(totalNum); 456 for (int i = 0; i < totalNum; ++i) { 457 removedIds.add(String.valueOf(i)); 458 } 459 460 contactsIndexerImpl.batchRemoveContactsAsync(removedIds, 461 mUpdateStats, /*shouldKeepUpdatingOnError=*/ true).get(); 462 463 assertThat(mAppSearchHelper.mRemovedIds).hasSize(removedIds.size()); 464 assertThat(mAppSearchHelper.mRemovedIds).isEqualTo(removedIds); 465 } 466 467 @Test testContactsIndexerImpl_batchUpdateContactsNullCursor_shouldContinueOnError()468 public void testContactsIndexerImpl_batchUpdateContactsNullCursor_shouldContinueOnError() 469 throws Exception { 470 MockContentResolver spyContentResolver = spy(mMockContentResolver); 471 doCallRealMethod() 472 .doReturn(null) 473 .doCallRealMethod() 474 .when(spyContentResolver).query(any(), any(), any(), any(), any()); 475 Context spyContext = new ContextWrapper(ApplicationProvider.getApplicationContext()) { 476 @Override 477 public ContentResolver getContentResolver() { 478 return spyContentResolver; 479 } 480 }; 481 ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(spyContext, 482 mAppSearchHelper); 483 484 // Insert contacts 485 ContentResolver resolver = mContext.getContentResolver(); 486 ContentValues dummyValues = new ContentValues(); 487 int totalNum = 3 * ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2; 488 for (int i = 0; i < totalNum; i++) { 489 resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues); 490 } 491 492 // Index three batches of contacts 493 List<String> wantedIds = new ArrayList<>(totalNum); 494 for (int i = 0; i < totalNum; ++i) { 495 wantedIds.add(String.valueOf(i)); 496 } 497 contactsIndexerImpl.batchUpdateContactsAsync(wantedIds, 498 mUpdateStats, /*shouldKeepUpdatingOnError=*/ true).get(); 499 500 // Contacts indexer should have failed on the second batch but continued to the third batch 501 List<String> ids = new ArrayList<>( 502 2 * ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2); 503 for (int i = 0; i < ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2; ++i) { 504 ids.add(String.valueOf(i)); 505 } 506 for (int i = 2 * ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2; i < totalNum; ++i) { 507 ids.add(String.valueOf(i)); 508 } 509 List<String> indexedIds = mAppSearchHelper.mIndexedContacts.stream().map( 510 Person::getId).collect(Collectors.toList()); 511 assertThat(indexedIds).isEqualTo(ids); 512 verify(spyContentResolver, times(3)).query(any(), any(), any(), any(), any()); 513 } 514 515 @Test testContactsIndexerImpl_batchUpdateContactsNullCursor_shouldNotContinueOnError()516 public void testContactsIndexerImpl_batchUpdateContactsNullCursor_shouldNotContinueOnError() { 517 MockContentResolver spyContentResolver = spy(mMockContentResolver); 518 doCallRealMethod() 519 .doReturn(null) 520 .doCallRealMethod() 521 .when(spyContentResolver).query(any(), any(), any(), any(), any()); 522 Context spyContext = new ContextWrapper(ApplicationProvider.getApplicationContext()) { 523 @Override 524 public ContentResolver getContentResolver() { 525 return spyContentResolver; 526 } 527 }; 528 ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(spyContext, 529 mAppSearchHelper); 530 531 // Insert contacts 532 ContentResolver resolver = mContext.getContentResolver(); 533 ContentValues dummyValues = new ContentValues(); 534 int totalNum = 3 * ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2; 535 for (int i = 0; i < totalNum; i++) { 536 resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues); 537 } 538 539 // Index three batches of contacts 540 List<String> wantedIds = new ArrayList<>(totalNum); 541 for (int i = 0; i < totalNum; ++i) { 542 wantedIds.add(String.valueOf(i)); 543 } 544 CompletableFuture<Void> future = contactsIndexerImpl.batchUpdateContactsAsync(wantedIds, 545 mUpdateStats, /*shouldKeepUpdatingOnError=*/ false); 546 ExecutionException thrown = assertThrows(ExecutionException.class, future::get); 547 assertThat(thrown).hasMessageThat().contains( 548 "Cursor was returned as null while querying CP2."); 549 550 // Contacts indexer should have stopped indexing on/before the second batch 551 assertThat(mAppSearchHelper.mIndexedContacts.size()).isAtMost( 552 ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2); 553 verify(spyContentResolver, times(2)).query(any(), any(), any(), any(), any()); 554 } 555 556 @Test testContactsIndexerImpl_batchUpdateContactsThrows_shouldContinueOnError()557 public void testContactsIndexerImpl_batchUpdateContactsThrows_shouldContinueOnError() 558 throws Exception { 559 MockContentResolver spyContentResolver = spy(mMockContentResolver); 560 doCallRealMethod() 561 .doThrow(new RuntimeException("mock exception")) 562 .doCallRealMethod() 563 .when(spyContentResolver).query(any(), any(), any(), any(), any()); 564 Context spyContext = new ContextWrapper(ApplicationProvider.getApplicationContext()) { 565 @Override 566 public ContentResolver getContentResolver() { 567 return spyContentResolver; 568 } 569 }; 570 ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(spyContext, 571 mAppSearchHelper); 572 573 // Insert contacts 574 ContentResolver resolver = mContext.getContentResolver(); 575 ContentValues dummyValues = new ContentValues(); 576 int totalNum = 3 * ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2; 577 for (int i = 0; i < totalNum; i++) { 578 resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues); 579 } 580 581 // Index three batches of contacts 582 List<String> wantedIds = new ArrayList<>(totalNum); 583 for (int i = 0; i < totalNum; ++i) { 584 wantedIds.add(String.valueOf(i)); 585 } 586 contactsIndexerImpl.batchUpdateContactsAsync(wantedIds, 587 mUpdateStats, /*/*shouldKeepUpdatingOnError=*/ true).get(); 588 589 // Contacts indexer should have failed on the second batch but continued to the third batch 590 List<String> ids = new ArrayList<>( 591 2 * ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2); 592 for (int i = 0; i < ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2; ++i) { 593 ids.add(String.valueOf(i)); 594 } 595 for (int i = 2 * ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2; i < totalNum; ++i) { 596 ids.add(String.valueOf(i)); 597 } 598 List<String> indexedIds = mAppSearchHelper.mIndexedContacts.stream().map( 599 Person::getId).collect(Collectors.toList()); 600 assertThat(indexedIds).isEqualTo(ids); 601 verify(spyContentResolver, times(3)).query(any(), any(), any(), any(), any()); 602 } 603 604 @Test testContactsIndexerImpl_batchUpdateContactsThrows_shouldNotContinueOnError()605 public void testContactsIndexerImpl_batchUpdateContactsThrows_shouldNotContinueOnError() { 606 MockContentResolver spyContentResolver = spy(mMockContentResolver); 607 doCallRealMethod() 608 .doThrow(new RuntimeException("mock exception")) 609 .doCallRealMethod() 610 .when(spyContentResolver).query(any(), any(), any(), any(), any()); 611 Context spyContext = new ContextWrapper(ApplicationProvider.getApplicationContext()) { 612 @Override 613 public ContentResolver getContentResolver() { 614 return spyContentResolver; 615 } 616 }; 617 ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(spyContext, 618 mAppSearchHelper); 619 620 // Insert contacts 621 ContentResolver resolver = mContext.getContentResolver(); 622 ContentValues dummyValues = new ContentValues(); 623 int totalNum = 3 * ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2; 624 for (int i = 0; i < totalNum; i++) { 625 resolver.insert(ContactsContract.Contacts.CONTENT_URI, dummyValues); 626 } 627 628 // Index three batches of contacts 629 List<String> wantedIds = new ArrayList<>(totalNum); 630 for (int i = 0; i < totalNum; ++i) { 631 wantedIds.add(String.valueOf(i)); 632 } 633 CompletableFuture<Void> future = contactsIndexerImpl.batchUpdateContactsAsync(wantedIds, 634 mUpdateStats, /*shouldKeepUpdatingOnError=*/ false); 635 ExecutionException thrown = assertThrows(ExecutionException.class, future::get); 636 assertThat(thrown).hasMessageThat().contains("mock exception"); 637 638 // Contacts indexer should have stopped indexing on/before the second batch 639 assertThat(mAppSearchHelper.mIndexedContacts.size()).isAtMost( 640 ContactsIndexerImpl.NUM_CONTACTS_PER_BATCH_FOR_CP2); 641 verify(spyContentResolver, times(2)).query(any(), any(), any(), any(), any()); 642 } 643 } 644