1 /* 2 * Copyright (C) 2023 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.transformer; 18 19 import static android.provider.ContactsContract.AUTHORITY_URI; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.appsearch.GenericDocument; 24 import android.app.appsearch.SearchSpec; 25 import android.content.ContentUris; 26 import android.net.Uri; 27 import android.provider.ContactsContract; 28 import android.provider.ContactsContract.Contacts; 29 import android.util.ArraySet; 30 import android.util.Log; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.server.appsearch.contactsindexer.AppSearchHelper; 34 import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint; 35 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person; 36 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.Map; 40 import java.util.Objects; 41 import java.util.Set; 42 43 /** Contains various transforms for {@link Person} enterprise access. */ 44 final class PersonEnterpriseTransformer { 45 private static final String TAG = "AppSearchPersonEnterpri"; 46 47 // These constants are hidden in ContactsContract.Contacts 48 private static final Uri CORP_CONTENT_URI = 49 Uri.withAppendedPath(AUTHORITY_URI, "contacts_corp"); 50 private static final long ENTERPRISE_CONTACT_ID_BASE = 1000000000; 51 private static final String ENTERPRISE_CONTACT_LOOKUP_PREFIX = "c-"; 52 53 // Person externalUri should begin with "content://com.android.contacts/contacts/lookup" 54 private static final String CONTACTS_LOOKUP_URI_PREFIX = Contacts.CONTENT_LOOKUP_URI.toString(); 55 56 private static final List<String> PERSON_ACCESSIBLE_PROPERTIES = 57 List.of( 58 Person.PERSON_PROPERTY_NAME, 59 Person.PERSON_PROPERTY_GIVEN_NAME, 60 Person.PERSON_PROPERTY_MIDDLE_NAME, 61 Person.PERSON_PROPERTY_FAMILY_NAME, 62 Person.PERSON_PROPERTY_EXTERNAL_URI, 63 Person.PERSON_PROPERTY_ADDITIONAL_NAME_TYPES, 64 Person.PERSON_PROPERTY_ADDITIONAL_NAMES, 65 Person.PERSON_PROPERTY_IMAGE_URI, 66 Person.PERSON_PROPERTY_CONTACT_POINTS 67 + "." 68 + ContactPoint.CONTACT_POINT_PROPERTY_LABEL, 69 Person.PERSON_PROPERTY_CONTACT_POINTS 70 + "." 71 + ContactPoint.CONTACT_POINT_PROPERTY_EMAIL, 72 Person.PERSON_PROPERTY_CONTACT_POINTS 73 + "." 74 + ContactPoint.CONTACT_POINT_PROPERTY_TELEPHONE); 75 76 @VisibleForTesting 77 static final Set<String> PERSON_ACCESSIBLE_PROPERTIES_SET = 78 new ArraySet<>(PERSON_ACCESSIBLE_PROPERTIES); 79 PersonEnterpriseTransformer()80 private PersonEnterpriseTransformer() {} 81 82 /** 83 * Returns whether or not a document of the given package, database, and schema type combination 84 * should be transformed for enterprise. 85 */ shouldTransform( @onNull String packageName, @NonNull String databaseName, @NonNull String schemaType)86 static boolean shouldTransform( 87 @NonNull String packageName, @NonNull String databaseName, @NonNull String schemaType) { 88 return schemaType.equals(Person.SCHEMA_TYPE) 89 && packageName.equals("android") 90 && databaseName.equals(AppSearchHelper.DATABASE_NAME); 91 } 92 93 /** 94 * Transforms the imageUri and externalUri properties of a Person document to their enterprise 95 * versions which are the corp thumbnail uri and corp lookup uri respectively. 96 * 97 * <p>When contacts are accessed through CP2's enterprise uri, CP2 replaces the contact id with 98 * an enterprise contact id (the original contact id plus a base enterprise id {@link 99 * ContactsContract.Contacts#ENTERPRISE_CONTACT_ID_BASE}). The corp thumbnail uri keeps the 100 * original contact id, but the corp lookup uri uses the enterprise contact id. 101 * 102 * <p>In this method, we only transform the imageUri and externalUri properties, and we leave 103 * the document id untouched, since changing the document id would interfere with retrieving 104 * documents by id. 105 */ 106 @NonNull transformDocument(@onNull GenericDocument originalDocument)107 static GenericDocument transformDocument(@NonNull GenericDocument originalDocument) { 108 Objects.requireNonNull(originalDocument); 109 String imageUri = originalDocument.getPropertyString(Person.PERSON_PROPERTY_IMAGE_URI); 110 String externalUri = 111 originalDocument.getPropertyString(Person.PERSON_PROPERTY_EXTERNAL_URI); 112 // Only transform the properties if they're present in the document. If neither property is 113 // present, just return the original document 114 if (imageUri == null && externalUri == null) { 115 return originalDocument; 116 } 117 GenericDocument.Builder<GenericDocument.Builder<?>> transformedDocumentBuilder = 118 new GenericDocument.Builder<>(originalDocument); 119 if (imageUri != null) { 120 try { 121 long contactId = Long.parseLong(originalDocument.getId()); 122 transformedDocumentBuilder.setPropertyString( 123 Person.PERSON_PROPERTY_IMAGE_URI, getCorpImageUri(contactId)); 124 } catch (NumberFormatException e) { 125 Log.w(TAG, "Failed to set imageUri property", e); 126 } 127 } 128 if (externalUri != null) { 129 transformedDocumentBuilder.setPropertyString( 130 Person.PERSON_PROPERTY_EXTERNAL_URI, getCorpLookupUri(externalUri)); 131 } 132 return transformedDocumentBuilder.build(); 133 } 134 135 /** 136 * Returns the corp thumbnail uri for the given contact id. Note, the generated uri should 137 * include the original contact id as opposed to the enterprise contact id returned by CP2 which 138 * has {@link Contacts#ENTERPRISE_CONTACT_ID_BASE} added to it. 139 */ 140 @VisibleForTesting 141 @NonNull getCorpImageUri(long contactId)142 static String getCorpImageUri(long contactId) { 143 // https://cs.android.com/android/platform/superproject/main/+/main:packages/providers/ContactsProvider/src/com/android/providers/contacts/enterprise/EnterpriseContactsCursorWrapper.java;l=178;drc=a9d2c06a03a653954629ff10070ebbe4ea87d526 144 return ContentUris.appendId(CORP_CONTENT_URI.buildUpon(), contactId) 145 .appendPath(Contacts.Photo.CONTENT_DIRECTORY) 146 .toString(); 147 } 148 149 /** 150 * Transforms the given lookup uri to a corp lookup uri. This prepends an enterprise-specific 151 * prefix to the lookup key segment and transforms the contact id segment (if present) to an 152 * enterprise contact id, e.g. "content://com.android.contacts/contacts/lookup/key/123" would 153 * become "content://com.android.contacts/contacts/lookup/c-key/1000000123". 154 * 155 * <p>Note, if the given lookup uri does not match a CP2 lookup uri, this just returns the 156 * original string. 157 */ 158 @VisibleForTesting 159 @NonNull getCorpLookupUri(@onNull String lookupUri)160 static String getCorpLookupUri(@NonNull String lookupUri) { 161 // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/provider/ContactsContract.java;l=2269;drc=9b279fbc71a1908311c32c24b4c65e967598b288 162 if (!lookupUri.startsWith(CONTACTS_LOOKUP_URI_PREFIX)) { 163 return lookupUri; 164 } 165 // The indexed uri has four segments: "contacts", "lookup", the lookup key, and the contact 166 // id (contact id is optional) 167 List<String> pathSegments = Uri.parse(lookupUri).getPathSegments(); 168 if (pathSegments.size() < 3) { 169 return lookupUri; 170 } 171 if (pathSegments.size() > 3) { 172 try { 173 long contactId = Long.parseLong(pathSegments.get(3)); 174 return getCorpLookupUriFromLookupKey(pathSegments.get(2), contactId); 175 } catch (NumberFormatException e) { 176 Log.w(TAG, "Failed to get contact id from lookup uri", e); 177 } 178 } 179 return getCorpLookupUriFromLookupKey(pathSegments.get(2)); 180 } 181 182 @NonNull getCorpLookupUriFromLookupKey(@onNull String lookupKey, long contactId)183 private static String getCorpLookupUriFromLookupKey(@NonNull String lookupKey, long contactId) { 184 return ContentUris.withAppendedId( 185 Uri.withAppendedPath( 186 Contacts.CONTENT_LOOKUP_URI, 187 ENTERPRISE_CONTACT_LOOKUP_PREFIX + lookupKey), 188 ENTERPRISE_CONTACT_ID_BASE + contactId) 189 .toString(); 190 } 191 192 @NonNull getCorpLookupUriFromLookupKey(@onNull String lookupKey)193 private static String getCorpLookupUriFromLookupKey(@NonNull String lookupKey) { 194 return Uri.withAppendedPath( 195 Contacts.CONTENT_LOOKUP_URI, ENTERPRISE_CONTACT_LOOKUP_PREFIX + lookupKey) 196 .toString(); 197 } 198 199 /** 200 * Transforms a {@link SearchSpec} through its builder, adding property filters and projections 201 * that restrict the allowed properties for the {@link Person} schema type. 202 */ transformSearchSpec( @onNull SearchSpec searchSpec, @NonNull SearchSpec.Builder builder)203 static void transformSearchSpec( 204 @NonNull SearchSpec searchSpec, @NonNull SearchSpec.Builder builder) { 205 Map<String, List<String>> projections = searchSpec.getProjections(); 206 Map<String, List<String>> filterProperties = searchSpec.getFilterProperties(); 207 builder.addProjection( 208 Person.SCHEMA_TYPE, getAccessibleProperties(projections.get(Person.SCHEMA_TYPE))); 209 builder.addFilterProperties( 210 Person.SCHEMA_TYPE, 211 getAccessibleProperties(filterProperties.get(Person.SCHEMA_TYPE))); 212 } 213 214 /** 215 * Adds allowed properties to the map for each {@link Person} schema type. If properties already 216 * exist in the map for {@link Person}, removes the unallowed properties, leaving an 217 * intersection of the original properties and the allowed properties. 218 */ transformPropertiesMap(@onNull Map<String, List<String>> propertiesMap)219 static void transformPropertiesMap(@NonNull Map<String, List<String>> propertiesMap) { 220 propertiesMap.put( 221 Person.SCHEMA_TYPE, getAccessibleProperties(propertiesMap.get(Person.SCHEMA_TYPE))); 222 } 223 224 /** 225 * If properties is non-null, returns the intersection of properties with the enterprise 226 * accessible properties for {@link Person}; otherwise simply returns the enterprise accessible 227 * properties for {@link Person}. 228 */ getAccessibleProperties(@ullable List<String> properties)229 private static List<String> getAccessibleProperties(@Nullable List<String> properties) { 230 if (properties == null) { 231 return PERSON_ACCESSIBLE_PROPERTIES; 232 } 233 List<String> filteredProperties = new ArrayList<>(); 234 for (int i = 0; i < properties.size(); i++) { 235 if (PERSON_ACCESSIBLE_PROPERTIES_SET.contains(properties.get(i))) { 236 filteredProperties.add(properties.get(i)); 237 } 238 } 239 return filteredProperties; 240 } 241 } 242