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