• 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 android.app.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
20 import static android.app.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
21 
22 import static com.google.common.truth.Truth.assertThat;
23 
24 import android.app.appsearch.AppSearchManager;
25 import android.app.appsearch.AppSearchResult;
26 import android.app.appsearch.AppSearchSession;
27 import android.app.appsearch.AppSearchSessionShim;
28 import android.app.appsearch.GenericDocument;
29 import android.app.appsearch.GetSchemaResponse;
30 import android.app.appsearch.PutDocumentsRequest;
31 import android.app.appsearch.SearchResultsShim;
32 import android.app.appsearch.SearchSpec;
33 import android.app.appsearch.SetSchemaRequest;
34 import android.app.appsearch.testutil.AppSearchSessionShimImpl;
35 import android.content.Context;
36 
37 import androidx.test.core.app.ApplicationProvider;
38 
39 import com.android.modules.utils.testing.TestableDeviceConfig;
40 import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
41 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
42 
43 import com.google.common.collect.ImmutableSet;
44 
45 import org.junit.After;
46 import org.junit.Before;
47 import org.junit.Rule;
48 import org.junit.Test;
49 
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.concurrent.CompletableFuture;
53 import java.util.concurrent.Executor;
54 import java.util.concurrent.Executors;
55 
56 // Since AppSearchHelper mainly just calls AppSearch's api to index/remove files, we shouldn't
57 // worry too much about it since AppSearch has good test coverage. Here just add some simple checks.
58 public class AppSearchHelperTest {
59     private final Executor mSingleThreadedExecutor = Executors.newSingleThreadExecutor();
60 
61     private Context mContext;
62     private AppSearchHelper mAppSearchHelper;
63     private ContactsUpdateStats mUpdateStats;
64 
65     private AppSearchSessionShim mDb;
66     private ContactsIndexerConfig mConfigForTest = new TestContactsIndexerConfig();
67 
68     @Before
setUp()69     public void setUp() throws Exception {
70         mContext = ApplicationProvider.getApplicationContext();
71         mUpdateStats = new ContactsUpdateStats();
72 
73         // b/258968096
74         // Internally AppSearchHelper.createAppSearchHelper will set Person and
75         // ContactPoint schema for AppSearch.
76         //
77         // Since everything is async, the fact we didn't wait until it finish is making
78         // testCreateAppSearchHelper_incompatibleSchemaChange flaky:
79         //   - In that test, it uses an AppSearchSessionShim to set
80         //   CONTACT_POINT_SCHEMA_WITH_LABEL_REPEATED
81         //   - Then, the test will create another AppSearchHelper
82         //   - For this local AppSearchHelper in the test, we are expecting an incompatible
83         //   schema change.
84         //   - But if mAppSearchHelper doesn't finish setting its schema, and
85         //   CONTACT_POINT_SCHEMA_WITH_LABEL_REPEATED is set first, mAppSearchHelper will get an
86         //   incompatible schema change, and the one created later for the test won't since it
87         //   will set the same schemas as mAppSearchHelper.
88         //
89         // To fix the flakiness, we need to wait until mAppSearchHelper finishes initialization.
90         // We choose to do it in the setup to make sure it won't create such flakiness in the
91         // future tests.
92         //
93         mAppSearchHelper = AppSearchHelper.createAppSearchHelper(mContext, mSingleThreadedExecutor,
94                 mConfigForTest);
95         // TODO(b/237115318) we need to revisit this once the contact indexer is refactored.
96         // getSession here will call get() on the future for AppSearchSession to make sure it has
97         // been initialized.
98         AppSearchSession unused = mAppSearchHelper.getSession();
99         AppSearchManager.SearchContext searchContext =
100                 new AppSearchManager.SearchContext.Builder(AppSearchHelper.DATABASE_NAME).build();
101         mDb = AppSearchSessionShimImpl.createSearchSessionAsync(
102                 searchContext).get();
103     }
104 
105     @After
tearDown()106     public void tearDown() throws Exception {
107         // Wipe the data in AppSearchHelper.DATABASE_NAME.
108         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
109                 .setForceOverride(true).build();
110         mDb.setSchemaAsync(setSchemaRequest).get();
111     }
112 
113     @Test
testAppSearchHelper_permissionIsSetCorrectlyForPerson()114     public void testAppSearchHelper_permissionIsSetCorrectlyForPerson() throws Exception {
115         // TODO(b/203605504) We can create AppSearchHelper in the test itself so make things more
116         //  clear.
117         AppSearchSession session = mAppSearchHelper.getSession();
118         CompletableFuture<AppSearchResult<GetSchemaResponse>> responseFuture =
119                 new CompletableFuture<>();
120 
121         // TODO(b/203605504) Considering using AppSearchShim, which is our test utility that
122         //  glues AppSearchSession to the Future API
123         session.getSchema(mSingleThreadedExecutor, responseFuture::complete);
124 
125         AppSearchResult<GetSchemaResponse> result = responseFuture.get();
126         assertThat(result.isSuccess()).isTrue();
127         GetSchemaResponse response = result.getResultValue();
128         assertThat(response.getRequiredPermissionsForSchemaTypeVisibility()).hasSize(2);
129         assertThat(response.getRequiredPermissionsForSchemaTypeVisibility()).containsKey(
130                 ContactPoint.SCHEMA_TYPE);
131         assertThat(response.getRequiredPermissionsForSchemaTypeVisibility()).containsEntry(
132                 Person.SCHEMA_TYPE,
133                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_CONTACTS)));
134     }
135 
136     @Test
testIndexContacts()137     public void testIndexContacts() throws Exception {
138         mAppSearchHelper.indexContactsAsync(generatePersonData(50), mUpdateStats).get();
139 
140         List<String> appsearchIds = mAppSearchHelper.getAllContactIdsAsync().get();
141         assertThat(appsearchIds.size()).isEqualTo(50);
142     }
143 
144     @Test
testIndexContacts_clearAfterIndex()145     public void testIndexContacts_clearAfterIndex() throws Exception {
146         List<Person> contacts = generatePersonData(50);
147 
148         CompletableFuture<Void> indexContactsFuture = mAppSearchHelper.indexContactsAsync(contacts,
149                 mUpdateStats);
150         contacts.clear();
151         indexContactsFuture.get();
152 
153         List<String> appsearchIds = mAppSearchHelper.getAllContactIdsAsync().get();
154         assertThat(appsearchIds.size()).isEqualTo(50);
155     }
156 
157     @Test
testAppSearchHelper_removeContacts()158     public void testAppSearchHelper_removeContacts() throws Exception {
159         mAppSearchHelper.indexContactsAsync(generatePersonData(50), mUpdateStats).get();
160         List<String> indexedIds = mAppSearchHelper.getAllContactIdsAsync().get();
161 
162         List<String> deletedIds = new ArrayList<>();
163         for (int i = 0; i < 50; i += 5) {
164             deletedIds.add(String.valueOf(i));
165         }
166         mAppSearchHelper.removeContactsByIdAsync(deletedIds, mUpdateStats).get();
167 
168         assertThat(indexedIds.size()).isEqualTo(50);
169         List<String> appsearchIds = mAppSearchHelper.getAllContactIdsAsync().get();
170         assertThat(appsearchIds).containsNoneIn(deletedIds);
171     }
172 
173     @Test
testCreateAppSearchHelper_compatibleSchemaChange()174     public void testCreateAppSearchHelper_compatibleSchemaChange() throws Exception {
175         AppSearchHelper appSearchHelper = AppSearchHelper.createAppSearchHelper(mContext,
176                 mSingleThreadedExecutor, mConfigForTest);
177 
178         assertThat(appSearchHelper).isNotNull();
179         assertThat(appSearchHelper.isDataLikelyWipedDuringInitAsync().get()).isFalse();
180     }
181 
182     @Test
testCreateAppSearchHelper_compatibleSchemaChange2()183     public void testCreateAppSearchHelper_compatibleSchemaChange2() throws Exception {
184         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
185                 .addSchemas(TestUtils.CONTACT_POINT_SCHEMA_WITH_APP_IDS_OPTIONAL)
186                 .setForceOverride(true).build();
187         mDb.setSchemaAsync(setSchemaRequest).get();
188 
189         // APP_IDS changed from optional to repeated, which is a compatible change.
190         AppSearchHelper appSearchHelper =
191                 AppSearchHelper.createAppSearchHelper(mContext, mSingleThreadedExecutor,
192                         mConfigForTest);
193 
194         assertThat(appSearchHelper).isNotNull();
195         assertThat(appSearchHelper.isDataLikelyWipedDuringInitAsync().get()).isFalse();
196     }
197 
198     @Test
testCreateAppSearchHelper_incompatibleSchemaChange()199     public void testCreateAppSearchHelper_incompatibleSchemaChange() throws Exception {
200         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
201                 .addSchemas(TestUtils.CONTACT_POINT_SCHEMA_WITH_LABEL_REPEATED)
202                 .setForceOverride(true).build();
203         mDb.setSchemaAsync(setSchemaRequest).get();
204 
205         // LABEL changed from repeated to optional, which is an incompatible change.
206         AppSearchHelper appSearchHelper =
207                 AppSearchHelper.createAppSearchHelper(mContext, mSingleThreadedExecutor,
208                         mConfigForTest);
209 
210         assertThat(appSearchHelper).isNotNull();
211         assertThat(appSearchHelper.isDataLikelyWipedDuringInitAsync().get()).isTrue();
212     }
213 
214     @Test
testGetAllContactIds()215     public void testGetAllContactIds() throws Exception {
216         indexContactsInBatchesAsync(generatePersonData(200)).get();
217 
218         List<String> appSearchContactIds = mAppSearchHelper.getAllContactIdsAsync().get();
219 
220         assertThat(appSearchContactIds.size()).isEqualTo(200);
221     }
222 
indexContactsInBatchesAsync(List<Person> contacts)223     private CompletableFuture<Void> indexContactsInBatchesAsync(List<Person> contacts) {
224         CompletableFuture<Void> indexContactsInBatchesFuture =
225                 CompletableFuture.completedFuture(null);
226         int startIndex = 0;
227         while (startIndex < contacts.size()) {
228             int batchEndIndex = Math.min(
229                     startIndex + ContactsIndexerImpl.NUM_UPDATED_CONTACTS_PER_BATCH_FOR_APPSEARCH,
230                     contacts.size());
231             List<Person> batchedContacts = contacts.subList(startIndex, batchEndIndex);
232             indexContactsInBatchesFuture = indexContactsInBatchesFuture
233                     .thenCompose(x -> mAppSearchHelper.indexContactsAsync(batchedContacts,
234                             mUpdateStats));
235             startIndex = batchEndIndex;
236         }
237         return indexContactsInBatchesFuture;
238     }
239 
240     @Test
testPersonSchema_indexFirstMiddleAndLastNames()241     public void testPersonSchema_indexFirstMiddleAndLastNames() throws Exception {
242         // Override test config to index first, middle and last names.
243         ContactsIndexerConfig config = new TestContactsIndexerConfig() {
244             @Override
245             public boolean shouldIndexFirstMiddleAndLastNames() {
246                 return true;
247             }
248         };
249         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
250                 .addSchemas(ContactPoint.SCHEMA, Person.getSchema(config))
251                 .setForceOverride(true).build();
252         mDb.setSchemaAsync(setSchemaRequest).get();
253         // Index document
254         GenericDocument doc1 =
255                 new GenericDocument.Builder<>("namespace", "id1", Person.SCHEMA_TYPE)
256                         .setPropertyString(Person.PERSON_PROPERTY_NAME, "新中野")
257                         .setPropertyString(Person.PERSON_PROPERTY_FAMILY_NAME, "新")
258                         .setPropertyString(Person.PERSON_PROPERTY_GIVEN_NAME, "野")
259                         .setPropertyString(Person.PERSON_PROPERTY_MIDDLE_NAME, "中")
260                         .build();
261         checkIsBatchResultSuccess(
262                 mDb.putAsync(
263                         new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
264 
265         SearchSpec spec = new SearchSpec.Builder()
266                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
267                 .build();
268 
269         // Searching by full name returns document
270         SearchResultsShim searchResults = mDb.search("新中野", spec);
271         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
272         assertThat(documents).containsExactly(doc1);
273 
274         // Searching by last name returns document
275         searchResults = mDb.search("新", spec);
276         documents = convertSearchResultsToDocuments(searchResults);
277         assertThat(documents).containsExactly(doc1);
278 
279         // Searching by middle name returns document
280         searchResults = mDb.search("中", spec);
281         documents = convertSearchResultsToDocuments(searchResults);
282         assertThat(documents).containsExactly(doc1);
283 
284         // Searching by first name returns document
285         searchResults = mDb.search("野", spec);
286         documents = convertSearchResultsToDocuments(searchResults);
287         assertThat(documents).containsExactly(doc1);
288     }
289 
290     // Index document only using the full name. This helps understand why first, middle
291     // and last names need to be indexed in order to be able to search some Chinese names
292     // efficiently. This can also potentially alert us of any future ICU tokenization changes.
293     // For e.g., if "新中野" is segmented to "新","中" and "野" in the future (as compared to only
294     // a single token "新中野" currently), the third and fourth asserts in ths test will start
295     // failing. This documents current behavior, but doesn't endorse it. Ideally, all of the below
296     // queries would be considered matches even when only the full name is indexed.
297     @Test
testPersonSchema_indexFullNameOnly()298     public void testPersonSchema_indexFullNameOnly() throws Exception {
299         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
300                 .addSchemas(ContactPoint.SCHEMA, Person.getSchema(mConfigForTest))
301                 .setForceOverride(true).build();
302         mDb.setSchemaAsync(setSchemaRequest).get();
303         GenericDocument doc1 =
304                 new GenericDocument.Builder<>("namespace", "id1", Person.SCHEMA_TYPE)
305                         .setPropertyString(Person.PERSON_PROPERTY_NAME, "新中野")
306                         .setPropertyString(Person.PERSON_PROPERTY_FAMILY_NAME, "新")
307                         .setPropertyString(Person.PERSON_PROPERTY_GIVEN_NAME, "野")
308                         .setPropertyString(Person.PERSON_PROPERTY_MIDDLE_NAME, "中")
309                         .build();
310         checkIsBatchResultSuccess(
311                 mDb.putAsync(
312                         new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
313 
314         // Searching by full name returns the document
315         SearchResultsShim searchResults =
316                 mDb.search(
317                         "新中野",
318                         new SearchSpec.Builder()
319                                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
320                                 .build());
321         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
322         assertThat(documents).containsExactly(doc1);
323 
324         SearchSpec spec = new SearchSpec.Builder()
325                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
326                 .build();
327 
328         // Searching by last name returns the document
329         searchResults = mDb.search("新", spec);
330         documents = convertSearchResultsToDocuments(searchResults);
331         assertThat(documents).containsExactly(doc1);
332 
333         // Searching by middle name doesn't return the document
334         searchResults = mDb.search("中", spec);
335         documents = convertSearchResultsToDocuments(searchResults);
336         assertThat(documents).isEmpty();
337 
338         // Searching by first name doesn't return the document
339         searchResults = mDb.search("野", spec);
340         documents = convertSearchResultsToDocuments(searchResults);
341         assertThat(documents).isEmpty();
342     }
343 
generatePersonData(int numContacts)344     List<Person> generatePersonData(int numContacts) {
345         List<Person> personList = new ArrayList<>();
346         for (int i = 0; i < numContacts; i++) {
347             personList.add(
348                     new Person.Builder(/*namespace=*/ "", String.valueOf(i), "name" + i).build());
349         }
350         return personList;
351     }
352 }