• 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 android.annotation.NonNull;
22 import android.app.appsearch.AppSearchManager;
23 import android.app.appsearch.AppSearchSessionShim;
24 import android.app.appsearch.SetSchemaRequest;
25 import android.app.appsearch.testutil.AppSearchSessionShimImpl;
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.ContextWrapper;
29 import android.test.ProviderTestCase2;
30 import android.util.Pair;
31 
32 import androidx.test.core.app.ApplicationProvider;
33 
34 import com.android.server.appsearch.contactsindexer.ContactsIndexerImpl.ContactsBatcher;
35 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
36 
37 import com.google.common.collect.ImmutableList;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 import java.util.Objects;
42 import java.util.concurrent.ExecutionException;
43 
44 public class ContactsIndexerImplTest extends ProviderTestCase2<FakeContactsProvider> {
45     // TODO(b/203605504) we could just use AppSearchHelper.
46     private FakeAppSearchHelper mAppSearchHelper;
47     private ContactsUpdateStats mUpdateStats;
48 
ContactsIndexerImplTest()49     public ContactsIndexerImplTest() {
50         super(FakeContactsProvider.class, FakeContactsProvider.AUTHORITY);
51     }
52 
53     @Override
setUp()54     public void setUp() throws Exception {
55         super.setUp();
56         Context context = ApplicationProvider.getApplicationContext();
57         mContext = new ContextWrapper(context) {
58             @Override
59             public ContentResolver getContentResolver() {
60                 return getMockContentResolver();
61             }
62         };
63         mAppSearchHelper = new FakeAppSearchHelper(mContext);
64         mUpdateStats = new ContactsUpdateStats();
65     }
66 
67     @Override
tearDown()68     public void tearDown() throws Exception {
69         // Wipe the data in AppSearchHelper.DATABASE_NAME.
70         AppSearchManager.SearchContext searchContext =
71                 new AppSearchManager.SearchContext.Builder(AppSearchHelper.DATABASE_NAME).build();
72         AppSearchSessionShim db = AppSearchSessionShimImpl.createSearchSessionAsync(
73                 searchContext).get();
74         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
75                 .setForceOverride(true).build();
76         db.setSchemaAsync(setSchemaRequest).get();
77     }
78 
79     /**
80      * Helper method to run a delta update in the test.
81      *
82      * <p> Get is called on the futures to make this helper method synchronous.
83      *
84      * @param lastUpdatedTimestamp used as the "since" filter for updating the contacts.
85      * @param lastDeletedTimestamp used as the "since" filter for deleting the contacts.
86      * @return new (lastUpdatedTimestamp, lastDeletedTimestamp) pair after the update and deletion.
87      */
runDeltaUpdateOnContactsIndexerImpl( @onNull ContactsIndexerImpl indexerImpl, long lastUpdatedTimestamp, long lastDeletedTimestamp, @NonNull ContactsUpdateStats updateStats)88     private Pair<Long, Long> runDeltaUpdateOnContactsIndexerImpl(
89             @NonNull ContactsIndexerImpl indexerImpl,
90             long lastUpdatedTimestamp,
91             long lastDeletedTimestamp,
92             @NonNull ContactsUpdateStats updateStats)
93             throws ExecutionException, InterruptedException {
94         Objects.requireNonNull(indexerImpl);
95         Objects.requireNonNull(updateStats);
96         List<String> wantedContactIds = new ArrayList<>();
97         List<String> unWantedContactIds = new ArrayList<>();
98 
99         lastUpdatedTimestamp = ContactsProviderUtil.getUpdatedContactIds(mContext,
100                 lastUpdatedTimestamp, ContactsProviderUtil.UPDATE_LIMIT_NONE,
101                 wantedContactIds, /*stats=*/ null);
102         lastDeletedTimestamp = ContactsProviderUtil.getDeletedContactIds(mContext,
103                 lastDeletedTimestamp, unWantedContactIds, /*stats=*/ null);
104         indexerImpl.updatePersonCorpusAsync(wantedContactIds, unWantedContactIds,
105                 updateStats).get();
106 
107         return new Pair<>(lastUpdatedTimestamp, lastDeletedTimestamp);
108     }
109 
testBatcher_noFlushBeforeReachingLimit()110     public void testBatcher_noFlushBeforeReachingLimit() throws Exception {
111         int batchSize = 5;
112         ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize);
113 
114         for (int i = 0; i < batchSize - 1; ++i) {
115             batcher.add(new PersonBuilderHelper(/*id=*/ String.valueOf(i),
116                     new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/
117                             String.valueOf(i))).setCreationTimestampMillis(0), mUpdateStats);
118         }
119         batcher.getCompositeFuture().get();
120 
121         assertThat(mAppSearchHelper.mIndexedContacts).isEmpty();
122     }
123 
testBatcher_autoFlush()124     public void testBatcher_autoFlush() throws Exception {
125         int batchSize = 5;
126         ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize);
127 
128         for (int i = 0; i < batchSize; ++i) {
129             batcher.add(
130                     new PersonBuilderHelper(
131                             /*id=*/ String.valueOf(i),
132                             new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/
133                                     String.valueOf(i))
134                     ).setCreationTimestampMillis(0), mUpdateStats);
135         }
136         batcher.getCompositeFuture().get();
137 
138         assertThat(mAppSearchHelper.mIndexedContacts).hasSize(batchSize);
139         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0);
140         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0);
141     }
142 
testBatcher_contactFingerprintSame_notIndexed()143     public void testBatcher_contactFingerprintSame_notIndexed() throws Exception {
144         int batchSize = 2;
145         ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize);
146         PersonBuilderHelper builderHelper1 = new PersonBuilderHelper("id1",
147                 new Person.Builder("namespace", "id1", "name1")
148                         .setGivenName("given1")
149         ).setCreationTimestampMillis(0);
150         PersonBuilderHelper builderHelper2 = new PersonBuilderHelper("id2",
151                 new Person.Builder("namespace", "id2", "name2")
152                         .setGivenName("given2")
153         ).setCreationTimestampMillis(0);
154         mAppSearchHelper.setExistingContacts(ImmutableList.of(builderHelper1.buildPerson(),
155                 builderHelper2.buildPerson()));
156 
157         // Try to add the same contacts
158         batcher.add(builderHelper1, mUpdateStats);
159         batcher.add(builderHelper2, mUpdateStats);
160         batcher.getCompositeFuture().get();
161 
162         assertThat(mAppSearchHelper.mIndexedContacts).isEmpty();
163         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0);
164         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0);
165     }
166 
testBatcher_contactFingerprintDifferent_notIndexedButBatched()167     public void testBatcher_contactFingerprintDifferent_notIndexedButBatched() throws Exception {
168         int batchSize = 2;
169         ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize);
170         PersonBuilderHelper builderHelper1 = new PersonBuilderHelper("id1",
171                 new Person.Builder("namespace", "id1", "name1")
172                         .setGivenName("given1")
173         ).setCreationTimestampMillis(0);
174         PersonBuilderHelper builderHelper2 = new PersonBuilderHelper("id2",
175                 new Person.Builder("namespace", "id2", "name2")
176                         .setGivenName("given2")
177         ).setCreationTimestampMillis(0);
178         mAppSearchHelper.setExistingContacts(
179                 ImmutableList.of(builderHelper1.buildPerson(), builderHelper2.buildPerson()));
180 
181         PersonBuilderHelper sameAsContact1 = builderHelper1;
182         // use toBuilder once it works. Now it is not found due to @hide and not sure how since
183         // the test does depend on framework-appsearch.impl.
184         PersonBuilderHelper notSameAsContact2 = new PersonBuilderHelper("id2",
185                 new Person.Builder("namespace", "id2", "name2").setGivenName(
186                         "given2diff")
187         ).setCreationTimestampMillis(0);
188         batcher.add(sameAsContact1, mUpdateStats);
189         batcher.add(notSameAsContact2, mUpdateStats);
190         batcher.getCompositeFuture().get();
191 
192         assertThat(mAppSearchHelper.mIndexedContacts).isEmpty();
193         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0);
194         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1);
195     }
196 
testBatcher_contactFingerprintDifferent_Indexed()197     public void testBatcher_contactFingerprintDifferent_Indexed() throws Exception {
198         int batchSize = 2;
199         ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize);
200         PersonBuilderHelper contact1 = new PersonBuilderHelper("id1",
201                 new Person.Builder("namespace", "id1", "name1")
202                         .setGivenName("given1")
203         ).setCreationTimestampMillis(0);
204         PersonBuilderHelper contact2 = new PersonBuilderHelper("id2",
205                 new Person.Builder("namespace", "id2", "name2")
206                         .setGivenName("given2")
207         ).setCreationTimestampMillis(0);
208         PersonBuilderHelper contact3 = new PersonBuilderHelper("id3",
209                 new Person.Builder("namespace", "id3", "name3")
210                         .setGivenName("given3")
211         ).setCreationTimestampMillis(0);
212         mAppSearchHelper.setExistingContacts(
213                 ImmutableList.of(contact1.buildPerson(), contact2.buildPerson(),
214                         contact3.buildPerson()));
215 
216         PersonBuilderHelper sameAsContact1 = contact1;
217         // use toBuilder once it works. Now it is not found due to @hide and not sure how since
218         // the test does depend on framework-appsearch.impl.
219         PersonBuilderHelper notSameAsContact2 = new PersonBuilderHelper("id2",
220                 new Person.Builder("namespace", "id2", "name2").setGivenName(
221                         "given2diff")
222         ).setCreationTimestampMillis(0);
223         PersonBuilderHelper notSameAsContact3 = new PersonBuilderHelper("id3",
224                 new Person.Builder("namespace", "id3", "name3").setGivenName(
225                         "given3diff")
226         ).setCreationTimestampMillis(0);
227         batcher.add(sameAsContact1, mUpdateStats);
228         batcher.add(notSameAsContact2, mUpdateStats);
229         batcher.add(notSameAsContact3, mUpdateStats);
230         batcher.getCompositeFuture().get();
231 
232         assertThat(mAppSearchHelper.mIndexedContacts).isEmpty();
233         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(1);
234         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1);
235 
236         batcher.flushAsync(mUpdateStats).get();
237 
238         assertThat(mAppSearchHelper.mIndexedContacts).containsExactly(
239                 notSameAsContact2.buildPerson(),
240                 notSameAsContact3.buildPerson());
241         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0);
242         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0);
243     }
244 
testBatcher_contactFingerprintDifferent_IndexedWithOriginalCreationTimestamp()245     public void testBatcher_contactFingerprintDifferent_IndexedWithOriginalCreationTimestamp()
246             throws Exception {
247         int batchSize = 2;
248         long originalTs = System.currentTimeMillis();
249         ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize);
250         PersonBuilderHelper contact1 = new PersonBuilderHelper("id1",
251                 new Person.Builder("namespace", "id1", "name1")
252                         .setGivenName("given1")
253         ).setCreationTimestampMillis(originalTs);
254         PersonBuilderHelper contact2 = new PersonBuilderHelper("id2",
255                 new Person.Builder("namespace", "id2", "name2")
256                         .setGivenName("given2")
257         ).setCreationTimestampMillis(originalTs);
258         PersonBuilderHelper contact3 = new PersonBuilderHelper("id3",
259                 new Person.Builder("namespace", "id3", "name3")
260                         .setGivenName("given3")
261         ).setCreationTimestampMillis(originalTs);
262         mAppSearchHelper.setExistingContacts(
263                 ImmutableList.of(contact1.buildPerson(), contact2.buildPerson(),
264                         contact3.buildPerson()));
265         long updatedTs1 = originalTs + 1;
266         long updatedTs2 = originalTs + 2;
267         long updatedTs3 = originalTs + 3;
268         PersonBuilderHelper sameAsContact1 = new PersonBuilderHelper("id1",
269                 new Person.Builder("namespace", "id1", "name1")
270                         .setGivenName("given1")
271         ).setCreationTimestampMillis(updatedTs1);
272         // use toBuilder once it works. Now it is not found due to @hide and not sure how since
273         // the test does depend on framework-appsearch.impl.
274         PersonBuilderHelper notSameAsContact2 = new PersonBuilderHelper("id2",
275                 new Person.Builder("namespace", "id2", "name2").setGivenName(
276                         "given2diff")
277         ).setCreationTimestampMillis(updatedTs2);
278         PersonBuilderHelper notSameAsContact3 = new PersonBuilderHelper("id3",
279                 new Person.Builder("namespace", "id3", "name3").setGivenName(
280                         "given3diff")
281         ).setCreationTimestampMillis(updatedTs3);
282 
283         assertThat(sameAsContact1.buildPerson().getCreationTimestampMillis()).isEqualTo(updatedTs1);
284         assertThat(notSameAsContact2.buildPerson().getCreationTimestampMillis()).isEqualTo(
285                 updatedTs2);
286         assertThat(notSameAsContact3.buildPerson().getCreationTimestampMillis()).isEqualTo(
287                 updatedTs3);
288 
289         batcher.add(sameAsContact1, mUpdateStats);
290         batcher.add(notSameAsContact2, mUpdateStats);
291         batcher.add(notSameAsContact3, mUpdateStats);
292         batcher.flushAsync(mUpdateStats).get();
293 
294         assertThat(mAppSearchHelper.mIndexedContacts).hasSize(2);
295         assertThat(mAppSearchHelper.mExistingContacts.get(
296                 "id1").getGivenName()).isEqualTo("given1");
297         assertThat(mAppSearchHelper.mExistingContacts.get(
298                 "id2").getGivenName()).isEqualTo("given2diff");
299         assertThat(mAppSearchHelper.mExistingContacts.get(
300                 "id3").getGivenName()).isEqualTo("given3diff");
301         // But the timestamps remain same.
302         assertThat(mAppSearchHelper.mExistingContacts.get(
303                 "id1").getCreationTimestampMillis()).isEqualTo(originalTs);
304         assertThat(mAppSearchHelper.mExistingContacts.get(
305                 "id2").getCreationTimestampMillis()).isEqualTo(originalTs);
306         assertThat(mAppSearchHelper.mExistingContacts.get(
307                 "id3").getCreationTimestampMillis()).isEqualTo(originalTs);
308     }
309 
testBatcher_contactNew_notIndexedButBatched()310     public void testBatcher_contactNew_notIndexedButBatched() throws Exception {
311         int batchSize = 2;
312         ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize);
313         PersonBuilderHelper contact1 = new PersonBuilderHelper("id1",
314                 new Person.Builder("namespace", "id1", "name1")
315                         .setGivenName("given1")
316         ).setCreationTimestampMillis(0);
317         mAppSearchHelper.setExistingContacts(ImmutableList.of(contact1.buildPerson()));
318 
319         PersonBuilderHelper sameAsContact1 = contact1;
320         // use toBuilder once it works. Now it is not found due to @hide and not sure how since
321         // the test does depend on framework-appsearch.impl.
322         PersonBuilderHelper newContact = new PersonBuilderHelper("id2",
323                 new Person.Builder("namespace", "id2", "name2").setGivenName(
324                         "given2diff")
325         ).setCreationTimestampMillis(0);
326         batcher.add(sameAsContact1, mUpdateStats);
327         batcher.add(newContact, mUpdateStats);
328         batcher.getCompositeFuture().get();
329 
330         assertThat(mAppSearchHelper.mIndexedContacts).isEmpty();
331         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0);
332         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1);
333     }
334 
testBatcher_contactNew_indexed()335     public void testBatcher_contactNew_indexed() throws Exception {
336         int batchSize = 2;
337         ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize);
338         PersonBuilderHelper contact1 = new PersonBuilderHelper("id1",
339                 new Person.Builder("namespace", "id1", "name1")
340                         .setGivenName("given1")).setCreationTimestampMillis(0);
341         mAppSearchHelper.setExistingContacts(ImmutableList.of(contact1.buildPerson()));
342 
343         PersonBuilderHelper sameAsContact1 = contact1;
344         // use toBuilder once it works. Now it is not found due to @hide and not sure how since
345         // the test does depend on framework-appsearch.impl.
346         PersonBuilderHelper newContact1 = new PersonBuilderHelper("id2",
347                 new Person.Builder("namespace", "id2", "name2").setGivenName(
348                         "given2diff")
349         ).setCreationTimestampMillis(0);
350         PersonBuilderHelper newContact2 = new PersonBuilderHelper("id3",
351                 new Person.Builder("namespace", "id3", "name3").setGivenName(
352                         "given3diff")
353         ).setCreationTimestampMillis(0);
354         batcher.add(sameAsContact1, mUpdateStats);
355         batcher.add(newContact1, mUpdateStats);
356         batcher.add(newContact2, mUpdateStats);
357         batcher.getCompositeFuture().get();
358 
359         assertThat(mAppSearchHelper.mIndexedContacts).isEmpty();
360         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(1);
361         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1);
362 
363         batcher.flushAsync(mUpdateStats).get();
364 
365         assertThat(mAppSearchHelper.mIndexedContacts).containsExactly(newContact1.buildPerson(),
366                 newContact2.buildPerson());
367         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0);
368         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0);
369     }
370 
testBatcher_batchedContactClearedAfterFlush()371     public void testBatcher_batchedContactClearedAfterFlush() throws Exception {
372         int batchSize = 5;
373         ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize);
374 
375         // First batch
376         for (int i = 0; i < batchSize; ++i) {
377             batcher.add(new PersonBuilderHelper(/*id=*/ String.valueOf(i),
378                     new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/
379                             String.valueOf(i))
380             ).setCreationTimestampMillis(0), mUpdateStats);
381         }
382         batcher.getCompositeFuture().get();
383 
384         assertThat(mAppSearchHelper.mIndexedContacts).hasSize(batchSize);
385         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0);
386         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0);
387 
388 
389         mAppSearchHelper.mIndexedContacts.clear();
390         // Second batch. Make sure the first batch has been cleared.
391         for (int i = 0; i < batchSize; ++i) {
392             batcher.add(new PersonBuilderHelper(/*id=*/ String.valueOf(i),
393                     new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/
394                             String.valueOf(i))
395                             // Different from previous ones to bypass the fingerprinting.
396                             .addNote("note")
397             ).setCreationTimestampMillis(0), mUpdateStats);
398         }
399         batcher.getCompositeFuture().get();
400 
401         assertThat(mAppSearchHelper.mIndexedContacts).hasSize(batchSize);
402         assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0);
403         assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0);
404     }
405 
testContactsIndexerImpl_batchRemoveContacts_largerThanBatchSize()406     public void testContactsIndexerImpl_batchRemoveContacts_largerThanBatchSize() throws Exception {
407         ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(mContext,
408                 mAppSearchHelper);
409         int totalNum = ContactsIndexerImpl.NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH + 1;
410         List<String> removedIds = new ArrayList<>(totalNum);
411         for (int i = 0; i < totalNum; ++i) {
412             removedIds.add(String.valueOf(i));
413         }
414 
415         contactsIndexerImpl.batchRemoveContactsAsync(removedIds, mUpdateStats).get();
416 
417         assertThat(mAppSearchHelper.mRemovedIds).hasSize(removedIds.size());
418         assertThat(mAppSearchHelper.mRemovedIds).isEqualTo(removedIds);
419     }
420 
testContactsIndexerImpl_batchRemoveContacts_smallerThanBatchSize()421     public void testContactsIndexerImpl_batchRemoveContacts_smallerThanBatchSize()
422             throws Exception {
423         ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(mContext,
424                 mAppSearchHelper);
425         int totalNum = ContactsIndexerImpl.NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH - 1;
426         List<String> removedIds = new ArrayList<>(totalNum);
427         for (int i = 0; i < totalNum; ++i) {
428             removedIds.add(String.valueOf(i));
429         }
430 
431         contactsIndexerImpl.batchRemoveContactsAsync(removedIds, mUpdateStats).get();
432 
433         assertThat(mAppSearchHelper.mRemovedIds).hasSize(removedIds.size());
434         assertThat(mAppSearchHelper.mRemovedIds).isEqualTo(removedIds);
435     }
436 }
437