1 /* 2 * Copyright (C) 2016 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.providers.contacts.enterprise; 18 19 import android.annotation.Nullable; 20 import android.content.ContentProvider; 21 import android.content.ContentUris; 22 import android.content.UriMatcher; 23 import android.database.Cursor; 24 import android.database.CursorWrapper; 25 import android.net.Uri; 26 import android.provider.ContactsContract; 27 import android.provider.MediaStore; 28 import android.provider.ContactsContract.Contacts; 29 import android.provider.ContactsContract.Data; 30 import android.provider.ContactsContract.Directory; 31 import android.provider.ContactsContract.PhoneLookup; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import com.android.internal.util.ArrayUtils; 36 import com.android.providers.contacts.ContactsProvider2; 37 38 /** 39 * Wrap cursor returned from work-side ContactsProvider in order to rewrite values in some colums 40 */ 41 public class EnterpriseContactsCursorWrapper extends CursorWrapper { 42 43 private static final String TAG = "EnterpriseCursorWrapper"; 44 private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 45 46 private static final UriMatcher sUriMatcher = ContactsProvider2.sUriMatcher; 47 48 // As some of the columns like PHOTO_URI requires contact id, but original projection may not 49 // have it, so caller may use a work projection instead of original project to make the 50 // query. Hence, we need also to restore the cursor to the origianl projection. 51 private final int[] contactIdIndices; 52 53 // Derived Fields 54 private final Long mDirectoryId; 55 private final boolean mIsDirectoryRemote; 56 private final String[] originalColumnNames; 57 58 public EnterpriseContactsCursorWrapper(Cursor cursor, String[] originalColumnNames, 59 int[] contactIdIndices, @Nullable Long directoryId) { 60 super(cursor); 61 this.contactIdIndices = contactIdIndices; 62 this.originalColumnNames = originalColumnNames; 63 this.mDirectoryId = directoryId; 64 this.mIsDirectoryRemote = directoryId != null 65 && Directory.isRemoteDirectoryId(directoryId); 66 } 67 68 @Override 69 public int getColumnCount() { 70 return originalColumnNames.length; 71 } 72 73 @Override 74 public String[] getColumnNames() { 75 return originalColumnNames; 76 } 77 78 @Override 79 public String getString(int columnIndex) { 80 final String result = super.getString(columnIndex); 81 final String columnName = super.getColumnName(columnIndex); 82 final long contactId = super.getLong(contactIdIndices[0]); 83 switch (columnName) { 84 case Contacts.PHOTO_THUMBNAIL_URI: 85 if(mIsDirectoryRemote) { 86 return getRemoteDirectoryFileUri(result); 87 } else { 88 return getCorpThumbnailUri(contactId, getWrappedCursor()); 89 } 90 case Contacts.PHOTO_URI: 91 if(mIsDirectoryRemote) { 92 return getRemoteDirectoryFileUri(result); 93 } else { 94 return getCorpDisplayPhotoUri(contactId, getWrappedCursor()); 95 } 96 case Data.PHOTO_FILE_ID: 97 case Data.PHOTO_ID: 98 return null; 99 case Data.CUSTOM_RINGTONE: 100 String ringtoneUri = super.getString(columnIndex); 101 // TODO: Remove this conditional block once accessing sounds in corp 102 // profile becomes possible. 103 if (ringtoneUri != null 104 && !Uri.parse(ringtoneUri).isPathPrefixMatch( 105 MediaStore.Audio.Media.INTERNAL_CONTENT_URI)) { 106 ringtoneUri = null; 107 } 108 return ringtoneUri; 109 case Contacts.LOOKUP_KEY: 110 final String lookupKey = super.getString(columnIndex); 111 if (TextUtils.isEmpty(lookupKey)) { 112 return null; 113 } else { 114 return Contacts.ENTERPRISE_CONTACT_LOOKUP_PREFIX + lookupKey; 115 } 116 default: 117 return result; 118 } 119 } 120 121 @Override 122 public int getInt(int column) { 123 return (int) getLong(column); 124 } 125 126 @Override 127 public long getLong(int column) { 128 long result = super.getLong(column); 129 if (ArrayUtils.contains(contactIdIndices, column)) { 130 return result + Contacts.ENTERPRISE_CONTACT_ID_BASE; 131 } else { 132 final String columnName = getColumnName(column); 133 switch (columnName) { 134 case Data.PHOTO_FILE_ID: 135 case Data.PHOTO_ID: 136 return 0; 137 default: 138 return result; 139 } 140 } 141 } 142 143 private String getRemoteDirectoryFileUri(final String photoUriString) { 144 if (photoUriString == null) { 145 return null; 146 } 147 148 // Assume that the authority of photoUri is directoryInfo.authority first 149 // TODO: Validate the authority of photoUri is directoryInfo.authority 150 Uri.Builder builder = Directory.ENTERPRISE_FILE_URI.buildUpon(); 151 builder.appendPath(photoUriString); 152 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, Long.toString(mDirectoryId)); 153 final String outputUri = builder.build().toString(); 154 if (VERBOSE_LOGGING) { 155 Log.v(TAG, "getCorpDirectoryFileUri: output URI=" + outputUri); 156 } 157 158 return outputUri; 159 } 160 161 /** 162 * Generate a photo URI for {@link PhoneLookup#PHOTO_THUMBNAIL_URI}. 163 * 164 * Example: "content://com.android.contacts/contacts_corp/ID/photo" 165 * 166 * {@link ContentProvider#openAssetFile} knows how to fetch from this URI. 167 */ 168 private static String getCorpThumbnailUri(long contactId, Cursor originalCursor) { 169 final int thumbnailUriIndex = originalCursor.getColumnIndex(Contacts.PHOTO_THUMBNAIL_URI); 170 final String thumbnailUri = originalCursor.getString(thumbnailUriIndex); 171 if (thumbnailUri == null) { 172 // No thumbnail. Just return null. 173 return null; 174 } 175 176 final int uriCode = sUriMatcher.match(Uri.parse(thumbnailUri)); 177 if (uriCode == ContactsProvider2.CONTACTS_ID_PHOTO) { 178 return ContentUris.appendId(Contacts.CORP_CONTENT_URI.buildUpon(), contactId) 179 .appendPath(Contacts.Photo.CONTENT_DIRECTORY).build().toString(); 180 } else { 181 Log.e(TAG, "EnterpriseContactsCursorWrapper contains invalid PHOTO_THUMBNAIL_URI"); 182 return null; 183 } 184 } 185 186 /** 187 * Generate a photo URI for {@link PhoneLookup#PHOTO_URI}. 188 * 189 * Example 1: "content://com.android.contacts/contacts_corp/ID/display_photo" 190 * Example 2: "content://com.android.contacts/contacts_corp/ID/photo" 191 * 192 * {@link ContentProvider#openAssetFile} knows how to fetch from this URI. 193 */ 194 private static String getCorpDisplayPhotoUri(long contactId, Cursor originalCursor) { 195 final int photoUriIndex = originalCursor.getColumnIndex(Contacts.PHOTO_URI); 196 final String photoUri = originalCursor.getString(photoUriIndex); 197 if (photoUri == null) { 198 return null; 199 } 200 201 final int uriCode = sUriMatcher.match(Uri.parse(photoUri)); 202 if (uriCode == ContactsProvider2.CONTACTS_ID_PHOTO) { 203 return ContentUris.appendId(Contacts.CORP_CONTENT_URI.buildUpon(), contactId) 204 .appendPath(Contacts.Photo.CONTENT_DIRECTORY).build().toString(); 205 } else if (uriCode == ContactsProvider2.CONTACTS_ID_DISPLAY_PHOTO 206 || uriCode == ContactsProvider2.DISPLAY_PHOTO_ID) { 207 return ContentUris.appendId(Contacts.CORP_CONTENT_URI.buildUpon(), contactId) 208 .appendPath(Contacts.Photo.DISPLAY_PHOTO).build().toString(); 209 } else { 210 Log.e(TAG, "EnterpriseContactsCursorWrapper contains invalid PHOTO_URI"); 211 return null; 212 } 213 } 214 }