1 /*
2  * Copyright (C) 2024 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 android.appsearch.app.helper_a;
18 
19 import static android.app.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
20 import static android.app.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
21 
22 import static com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint.CONTACT_POINT_PROPERTY_ADDRESS;
23 import static com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint.CONTACT_POINT_PROPERTY_EMAIL;
24 import static com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint.CONTACT_POINT_PROPERTY_LABEL;
25 import static com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint.CONTACT_POINT_PROPERTY_TELEPHONE;
26 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_ADDITIONAL_NAMES;
27 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_ADDITIONAL_NAME_TYPES;
28 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_AFFILIATIONS;
29 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_CONTACT_POINTS;
30 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_EXTERNAL_URI;
31 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_FAMILY_NAME;
32 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_GIVEN_NAME;
33 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_IMAGE_URI;
34 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_MIDDLE_NAME;
35 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_NAME;
36 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.PERSON_PROPERTY_NOTES;
37 import static com.android.server.appsearch.contactsindexer.appsearchtypes.Person.TYPE_NICKNAME;
38 
39 import static com.google.common.truth.Truth.assertThat;
40 
41 import android.app.appsearch.AppSearchBatchResult;
42 import android.app.appsearch.AppSearchManager;
43 import android.app.appsearch.AppSearchSchema;
44 import android.app.appsearch.AppSearchSessionShim;
45 import android.app.appsearch.EnterpriseGlobalSearchSessionShim;
46 import android.app.appsearch.GenericDocument;
47 import android.app.appsearch.GetByDocumentIdRequest;
48 import android.app.appsearch.GetSchemaResponse;
49 import android.app.appsearch.PutDocumentsRequest;
50 import android.app.appsearch.SearchResultsShim;
51 import android.app.appsearch.SearchSpec;
52 import android.app.appsearch.SetSchemaRequest;
53 import android.app.appsearch.testutil.AppSearchSessionShimImpl;
54 import android.app.appsearch.testutil.EnterpriseGlobalSearchSessionShimImpl;
55 import android.app.appsearch.testutil.TestContactsIndexerConfig;
56 import android.net.Uri;
57 
58 import androidx.test.core.app.ApplicationProvider;
59 import androidx.test.ext.junit.runners.AndroidJUnit4;
60 
61 import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
62 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
63 
64 import com.google.common.collect.ImmutableSet;
65 
66 import org.junit.Before;
67 import org.junit.Test;
68 import org.junit.runner.RunWith;
69 
70 import java.util.Arrays;
71 import java.util.List;
72 import java.util.Map;
73 import java.util.Set;
74 
75 @RunWith(AndroidJUnit4.class)
76 public class EnterpriseContactsDeviceTest {
77     private static final String PACKAGE_NAME =
78             ApplicationProvider.getApplicationContext().getPackageName();
79     private static final String DATABASE_NAME = "contacts";
80 
81     // These constants are hidden in SetSchemaRequest
82     private static final int ENTERPRISE_ACCESS = 7;
83     private static final int MANAGED_PROFILE_CONTACTS_ACCESS = 8;
84 
85     private EnterpriseGlobalSearchSessionShim mEnterpriseSession;
86 
87     @Before
setUp()88     public void setUp() throws Exception {
89         mEnterpriseSession = EnterpriseGlobalSearchSessionShimImpl
90                 .createEnterpriseGlobalSearchSessionAsync().get();
91     }
92 
createPersonBuilder(String namespace, String id, String name)93     private Person.Builder createPersonBuilder(String namespace, String id, String name) {
94         return new Person.Builder(namespace, id, name)
95                 .setGivenName("givenName")
96                 .setMiddleName("middleName")
97                 .setFamilyName("familyName")
98                 .setExternalUri(Uri.parse("externalUri"))
99                 .setImageUri(Uri.parse("imageUri"))
100                 .setIsImportant(true)
101                 .setIsBot(true)
102                 .addAdditionalName(TYPE_NICKNAME, "nickname")
103                 .addAffiliation("affiliation")
104                 .addRelation("relation")
105                 .addNote("note");
106     }
107 
setUpEnterpriseContactsWithPermissions(Set<Integer> permissions)108     private void setUpEnterpriseContactsWithPermissions(Set<Integer> permissions) throws Exception {
109         AppSearchManager.SearchContext searchContext =
110                 new AppSearchManager.SearchContext.Builder(
111                         DATABASE_NAME).build();
112         AppSearchSessionShim db = AppSearchSessionShimImpl.createSearchSessionAsync(
113                 searchContext).get();
114         SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
115                 .addSchemas(ContactPoint.SCHEMA,
116                         Person.getSchema(new TestContactsIndexerConfig()))
117                 .addRequiredPermissionsForSchemaTypeVisibility(Person.SCHEMA_TYPE, permissions)
118                 .setForceOverride(true).build();
119         db.setSchemaAsync(setSchemaRequest).get();
120         // Index document
121         Person person1 = createPersonBuilder("namespace", "123", "Sam1 Curran")
122                 .addContactPoint(new ContactPoint
123                         .Builder("namespace", "cp1", "contact1")
124                         .addEmail("person1@email.com")
125                         .addPhone("123456")
126                         .addAppId("appId1")
127                         .addAddress("address1")
128                         .build())
129                 .build();
130         Person person2 = createPersonBuilder("namespace", "1234", "Sam2 Curran")
131                 .addContactPoint(new ContactPoint
132                         .Builder("namespace", "cp2", "contact2")
133                         .addEmail("person2@email.com")
134                         .addPhone("1234567")
135                         .addAppId("appId2")
136                         .addAddress("address2")
137                         .build())
138                 .build();
139         Person person3 = createPersonBuilder("namespace", "12345", "Sam3 Curran")
140                 .addContactPoint(new ContactPoint
141                         .Builder("namespace", "cp3", "contact3")
142                         .addEmail("person3@email.com")
143                         .addPhone("12345678")
144                         .addAppId("appId3")
145                         .addAddress("address3")
146                         .build())
147                 .build();
148         checkIsBatchResultSuccess(db.putAsync(
149                 new PutDocumentsRequest.Builder().addGenericDocuments(person1, person2,
150                         person3).build()));
151     }
152 
153     @Test
setUpEnterpriseContacts()154     public void setUpEnterpriseContacts() throws Exception {
155         // In production, contacts are guarded by READ_CONTACTS permission; however, not only is
156         // that unnecessary to include in a test scenario, but the permission-granting infra in
157         // these tests is unreliable, so we omit that here.
158         setUpEnterpriseContactsWithPermissions(ImmutableSet.of(ENTERPRISE_ACCESS));
159     }
160 
161     @Test
setUpEnterpriseContactsWithoutEnterprisePermissions()162     public void setUpEnterpriseContactsWithoutEnterprisePermissions() throws Exception {
163         // In production, contacts are guarded by READ_CONTACTS permission; however, not only is
164         // that unnecessary to include in a test scenario, but the permission-granting infra in
165         // these tests is unreliable, so we omit that here.
166         setUpEnterpriseContactsWithPermissions(ImmutableSet.of());
167     }
168 
169     @Test
setUpEnterpriseContactsWithManagedPermission()170     public void setUpEnterpriseContactsWithManagedPermission() throws Exception {
171         // In production, contacts are guarded by READ_CONTACTS permission; however, not only is
172         // that unnecessary to include in a test scenario, but the permission-granting infra in
173         // these tests is unreliable, so we omit that here.
174         setUpEnterpriseContactsWithPermissions(ImmutableSet.of(ENTERPRISE_ACCESS,
175                 MANAGED_PROFILE_CONTACTS_ACCESS));
176     }
177 
178     @Test
testHasEnterpriseAccess()179     public void testHasEnterpriseAccess() throws Exception {
180         // Verify we can get the schema from the enterprise session
181         GetSchemaResponse getSchemaResponse = mEnterpriseSession.getSchemaAsync(PACKAGE_NAME,
182                 DATABASE_NAME).get();
183         Set<AppSearchSchema> schemas = getSchemaResponse.getSchemas();
184         assertThat(schemas).hasSize(1);
185         assertThat(schemas.iterator().next().getSchemaType()).isEqualTo(Person.SCHEMA_TYPE);
186 
187         // Searching with enterprise session returns documents
188         SearchSpec spec = new SearchSpec.Builder()
189                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
190                 .addFilterNamespaces("namespace")
191                 .build();
192         SearchResultsShim searchResults = mEnterpriseSession.search("", spec);
193         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
194         assertThat(documents).isNotEmpty();
195     }
196 
197     @Test
testDoesNotHaveEnterpriseAccess()198     public void testDoesNotHaveEnterpriseAccess() throws Exception {
199         // Verify we cannot get the schema from the enterprise session
200         GetSchemaResponse getSchemaResponse = mEnterpriseSession.getSchemaAsync(PACKAGE_NAME,
201                 DATABASE_NAME).get();
202         assertThat(getSchemaResponse.getSchemas()).isEmpty();
203 
204         // Searching with enterprise session doesn't return any documents
205         SearchSpec spec = new SearchSpec.Builder()
206                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
207                 .addFilterNamespaces("namespace")
208                 .build();
209         SearchResultsShim searchResults = mEnterpriseSession.search("", spec);
210         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
211         assertThat(documents).isEmpty();
212     }
213 
214     @Test
testGetEnterpriseContact()215     public void testGetEnterpriseContact() throws Exception {
216         GetByDocumentIdRequest getDocumentRequest = new GetByDocumentIdRequest.Builder(
217                 "namespace").addIds("123").build();
218 
219         AppSearchBatchResult<String, GenericDocument> getResult =
220                 mEnterpriseSession.getByDocumentIdAsync(
221                         ApplicationProvider.getApplicationContext().getPackageName(),
222                         DATABASE_NAME, getDocumentRequest).get();
223         //
224         assertThat(getResult.isSuccess()).isTrue();
225         GenericDocument document = getResult.getSuccesses().get("123");
226         assertThat(document.getPropertyNames()).containsAtLeast(PERSON_PROPERTY_NAME,
227                 PERSON_PROPERTY_GIVEN_NAME, PERSON_PROPERTY_MIDDLE_NAME,
228                 PERSON_PROPERTY_FAMILY_NAME, PERSON_PROPERTY_EXTERNAL_URI,
229                 PERSON_PROPERTY_ADDITIONAL_NAME_TYPES, PERSON_PROPERTY_ADDITIONAL_NAMES,
230                 PERSON_PROPERTY_IMAGE_URI, PERSON_PROPERTY_CONTACT_POINTS);
231         assertThat(document.getPropertyNames()).doesNotContain(PERSON_PROPERTY_NOTES);
232         assertThat(document.getPropertyString(PERSON_PROPERTY_NAME)).isEqualTo("Sam1 Curran");
233         assertThat(document.getPropertyString(PERSON_PROPERTY_GIVEN_NAME)).isEqualTo("givenName");
234         assertThat(document.getPropertyString(PERSON_PROPERTY_MIDDLE_NAME)).isEqualTo("middleName");
235         assertThat(document.getPropertyString(PERSON_PROPERTY_FAMILY_NAME)).isEqualTo("familyName");
236         assertThat(document.getPropertyString(PERSON_PROPERTY_EXTERNAL_URI)).isEqualTo(
237                 "externalUri");
238         assertThat(document.getPropertyLongArray(
239                 PERSON_PROPERTY_ADDITIONAL_NAME_TYPES)).asList().containsExactly(
240                 (long) TYPE_NICKNAME);
241         assertThat(document.getPropertyStringArray(
242                 PERSON_PROPERTY_ADDITIONAL_NAMES)).asList().containsExactly("nickname");
243         // The imageUri property will not be rewritten by EnterpriseSearchResultPageTransformer
244         // since this document does not come from the actual AppSearch contacts corpus
245         assertThat(document.getPropertyString(PERSON_PROPERTY_IMAGE_URI)).isEqualTo("imageUri");
246         GenericDocument contactPoint = document.getPropertyDocumentArray(
247                 PERSON_PROPERTY_CONTACT_POINTS)[0];
248         assertThat(contactPoint.getPropertyNames()).containsAtLeast(CONTACT_POINT_PROPERTY_LABEL,
249                 CONTACT_POINT_PROPERTY_EMAIL, CONTACT_POINT_PROPERTY_TELEPHONE);
250         assertThat(contactPoint.getPropertyNames()).doesNotContain(CONTACT_POINT_PROPERTY_ADDRESS);
251         assertThat(contactPoint.getPropertyString(CONTACT_POINT_PROPERTY_LABEL)).isEqualTo(
252                 "contact1");
253         assertThat(contactPoint.getPropertyString(CONTACT_POINT_PROPERTY_EMAIL)).isEqualTo(
254                 "person1@email.com");
255         assertThat(contactPoint.getPropertyString(CONTACT_POINT_PROPERTY_TELEPHONE)).isEqualTo(
256                 "123456");
257 
258         // Check projections were not overwritten across Binder
259         assertThat(getDocumentRequest.getProjections()).isEmpty();
260     }
261 
262     @Test
testGetEnterpriseContact_withProjection()263     public void testGetEnterpriseContact_withProjection() throws Exception {
264         GetByDocumentIdRequest getDocumentRequest = new GetByDocumentIdRequest.Builder(
265                 "namespace").addIds("123").addProjection(Person.SCHEMA_TYPE,
266                 Arrays.asList(PERSON_PROPERTY_NAME, PERSON_PROPERTY_ADDITIONAL_NAMES,
267                         PERSON_PROPERTY_CONTACT_POINTS + "." + CONTACT_POINT_PROPERTY_ADDRESS,
268                         PERSON_PROPERTY_CONTACT_POINTS + "."
269                                 + CONTACT_POINT_PROPERTY_EMAIL)).build();
270         Map<String, List<String>> projectionsCopy = getDocumentRequest.getProjections();
271 
272         AppSearchBatchResult<String, GenericDocument> getResult =
273                 mEnterpriseSession.getByDocumentIdAsync(
274                         ApplicationProvider.getApplicationContext().getPackageName(),
275                         DATABASE_NAME, getDocumentRequest).get();
276         assertThat(getResult.isSuccess()).isTrue();
277         GenericDocument document = getResult.getSuccesses().get("123");
278         assertThat(document.getPropertyNames()).containsExactly(PERSON_PROPERTY_NAME,
279                 PERSON_PROPERTY_CONTACT_POINTS, PERSON_PROPERTY_ADDITIONAL_NAMES);
280         assertThat(document.getPropertyString(PERSON_PROPERTY_NAME)).isEqualTo("Sam1 Curran");
281         assertThat(document.getPropertyStringArray(
282                 PERSON_PROPERTY_ADDITIONAL_NAMES)).asList().containsExactly("nickname");
283         GenericDocument contactPoint = document.getPropertyDocumentArray(
284                 PERSON_PROPERTY_CONTACT_POINTS)[0];
285         assertThat(contactPoint.getPropertyNames()).containsExactly(CONTACT_POINT_PROPERTY_EMAIL);
286         assertThat(contactPoint.getPropertyString(CONTACT_POINT_PROPERTY_EMAIL)).isEqualTo(
287                 "person1@email.com");
288         // CONTACT_POINT_PROPERTY_ADDRESS is not an accessible property
289         assertThat(contactPoint.getPropertyString(CONTACT_POINT_PROPERTY_ADDRESS)).isNull();
290 
291         // Check projections were not overwritten across Binder
292         assertThat(getDocumentRequest.getProjections()).isEqualTo(projectionsCopy);
293     }
294 
295     @Test
testSearchEnterpriseContacts()296     public void testSearchEnterpriseContacts() throws Exception {
297         SearchSpec spec = new SearchSpec.Builder()
298                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
299                 .addFilterNamespaces("namespace")
300                 .build();
301 
302         SearchResultsShim searchResults = mEnterpriseSession.search("", spec);
303         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
304         assertThat(documents).hasSize(3);
305         for (GenericDocument document : documents) {
306             assertThat(document.getPropertyNames()).containsAtLeast(PERSON_PROPERTY_NAME,
307                     PERSON_PROPERTY_GIVEN_NAME, PERSON_PROPERTY_MIDDLE_NAME,
308                     PERSON_PROPERTY_FAMILY_NAME, PERSON_PROPERTY_EXTERNAL_URI,
309                     PERSON_PROPERTY_ADDITIONAL_NAME_TYPES, PERSON_PROPERTY_ADDITIONAL_NAMES,
310                     PERSON_PROPERTY_IMAGE_URI, PERSON_PROPERTY_CONTACT_POINTS);
311             assertThat(document.getPropertyNames()).doesNotContain(PERSON_PROPERTY_NOTES);
312             GenericDocument contactPoint = document.getPropertyDocumentArray(
313                     PERSON_PROPERTY_CONTACT_POINTS)[0];
314             assertThat(contactPoint.getPropertyNames()).containsAtLeast(
315                     CONTACT_POINT_PROPERTY_LABEL, CONTACT_POINT_PROPERTY_EMAIL,
316                     CONTACT_POINT_PROPERTY_TELEPHONE);
317             assertThat(contactPoint.getPropertyNames()).doesNotContain(
318                     CONTACT_POINT_PROPERTY_ADDRESS);
319         }
320 
321         // Searching by indexed but inaccessible properties returns nothing
322         searchResults = mEnterpriseSession.search("affiliation OR note OR address", spec);
323         documents = convertSearchResultsToDocuments(searchResults);
324         assertThat(documents).isEmpty();
325     }
326 
327     @Test
testSearchEnterpriseContacts_withProjection()328     public void testSearchEnterpriseContacts_withProjection() throws Exception {
329         SearchSpec spec = new SearchSpec.Builder()
330                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
331                 .addFilterNamespaces("namespace")
332                 .addProjection(Person.SCHEMA_TYPE,
333                         Arrays.asList(PERSON_PROPERTY_NAME, PERSON_PROPERTY_ADDITIONAL_NAMES,
334                                 PERSON_PROPERTY_CONTACT_POINTS + "."
335                                         + CONTACT_POINT_PROPERTY_ADDRESS,
336                                 PERSON_PROPERTY_CONTACT_POINTS + "."
337                                         + CONTACT_POINT_PROPERTY_EMAIL))
338                 .build();
339 
340         SearchResultsShim searchResults = mEnterpriseSession.search("", spec);
341         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
342         assertThat(documents).hasSize(3);
343         for (GenericDocument document : documents) {
344             assertThat(document.getPropertyNames()).containsExactly(PERSON_PROPERTY_NAME,
345                     PERSON_PROPERTY_CONTACT_POINTS, PERSON_PROPERTY_ADDITIONAL_NAMES);
346             assertThat(document.getPropertyStringArray(
347                     PERSON_PROPERTY_ADDITIONAL_NAMES)).asList().containsExactly("nickname");
348             GenericDocument contactPoint = document.getPropertyDocumentArray(
349                     PERSON_PROPERTY_CONTACT_POINTS)[0];
350             assertThat(contactPoint.getPropertyNames()).containsExactly(
351                     CONTACT_POINT_PROPERTY_EMAIL);
352             // CONTACT_POINT_PROPERTY_ADDRESS is not an accessible property
353             assertThat(contactPoint.getPropertyString(CONTACT_POINT_PROPERTY_ADDRESS)).isNull();
354         }
355 
356         // Searching by indexed but inaccessible properties returns nothing
357         searchResults = mEnterpriseSession.search("affiliation OR note OR address", spec);
358         documents = convertSearchResultsToDocuments(searchResults);
359         assertThat(documents).isEmpty();
360     }
361 
362     @Test
testSearchEnterpriseContacts_withFilter()363     public void testSearchEnterpriseContacts_withFilter() throws Exception {
364         SearchSpec spec = new SearchSpec.Builder()
365                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
366                 .addFilterNamespaces("namespace")
367                 .addFilterProperties(Person.SCHEMA_TYPE,
368                         Arrays.asList(PERSON_PROPERTY_NAME, PERSON_PROPERTY_ADDITIONAL_NAMES,
369                                 PERSON_PROPERTY_AFFILIATIONS, PERSON_PROPERTY_NOTES))
370                 .build();
371 
372         // Searching by name and nickname returns results
373         SearchResultsShim searchResults = mEnterpriseSession.search("Sam AND nickname", spec);
374         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
375         assertThat(documents).hasSize(3);
376         for (GenericDocument document : documents) {
377             assertThat(document.getPropertyNames()).containsAtLeast(PERSON_PROPERTY_NAME,
378                     PERSON_PROPERTY_GIVEN_NAME, PERSON_PROPERTY_MIDDLE_NAME,
379                     PERSON_PROPERTY_FAMILY_NAME, PERSON_PROPERTY_EXTERNAL_URI,
380                     PERSON_PROPERTY_ADDITIONAL_NAME_TYPES, PERSON_PROPERTY_ADDITIONAL_NAMES,
381                     PERSON_PROPERTY_IMAGE_URI, PERSON_PROPERTY_CONTACT_POINTS);
382             assertThat(document.getPropertyNames()).doesNotContain(PERSON_PROPERTY_NOTES);
383             GenericDocument contactPoint = document.getPropertyDocumentArray(
384                     PERSON_PROPERTY_CONTACT_POINTS)[0];
385             assertThat(contactPoint.getPropertyNames()).containsAtLeast(
386                     CONTACT_POINT_PROPERTY_LABEL, CONTACT_POINT_PROPERTY_EMAIL,
387                     CONTACT_POINT_PROPERTY_TELEPHONE);
388             assertThat(contactPoint.getPropertyNames()).doesNotContain(
389                     CONTACT_POINT_PROPERTY_ADDRESS);
390         }
391 
392         // Searching by the filtered properties that are still inaccessible even when explicitly
393         // set returns nothing
394         searchResults = mEnterpriseSession.search("affiliation OR note OR address", spec);
395         documents = convertSearchResultsToDocuments(searchResults);
396         assertThat(documents).isEmpty();
397     }
398 }
399