• 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.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