1 /* 2 * Copyright (C) 2010 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.exchange.provider; 18 19 import com.android.emailcommon.Configuration; 20 import com.android.emailcommon.mail.PackedString; 21 import com.android.emailcommon.provider.Account; 22 import com.android.emailcommon.provider.EmailContent; 23 import com.android.emailcommon.provider.EmailContent.AccountColumns; 24 import com.android.emailcommon.service.AccountServiceProxy; 25 import com.android.emailcommon.utility.Utility; 26 import com.android.exchange.Eas; 27 import com.android.exchange.EasSyncService; 28 import com.android.exchange.R; 29 import com.android.exchange.provider.GalResult.GalData; 30 31 import android.accounts.AccountManager; 32 import android.content.ContentProvider; 33 import android.content.ContentValues; 34 import android.content.Context; 35 import android.content.UriMatcher; 36 import android.database.Cursor; 37 import android.database.MatrixCursor; 38 import android.net.Uri; 39 import android.os.Binder; 40 import android.os.Bundle; 41 import android.os.RemoteException; 42 import android.provider.ContactsContract; 43 import android.provider.ContactsContract.CommonDataKinds; 44 import android.provider.ContactsContract.CommonDataKinds.Email; 45 import android.provider.ContactsContract.CommonDataKinds.Phone; 46 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 47 import android.provider.ContactsContract.Contacts; 48 import android.provider.ContactsContract.Contacts.Data; 49 import android.provider.ContactsContract.Directory; 50 import android.provider.ContactsContract.RawContacts; 51 import android.text.TextUtils; 52 53 import java.util.HashMap; 54 import java.util.List; 55 56 /** 57 * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is 58 * used solely to provide GAL (Global Address Lookup) service to email address adapters 59 */ 60 public class ExchangeDirectoryProvider extends ContentProvider { 61 public static final String EXCHANGE_GAL_AUTHORITY = "com.android.exchange.directory.provider"; 62 63 private static final int DEFAULT_CONTACT_ID = 1; 64 private static final int DEFAULT_LOOKUP_LIMIT = 20; 65 66 private static final int GAL_BASE = 0; 67 private static final int GAL_DIRECTORIES = GAL_BASE; 68 private static final int GAL_FILTER = GAL_BASE + 1; 69 private static final int GAL_CONTACT = GAL_BASE + 2; 70 private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3; 71 private static final int GAL_EMAIL_FILTER = GAL_BASE + 4; 72 73 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 74 /*package*/ final HashMap<String, Long> mAccountIdMap = new HashMap<String, Long>(); 75 76 static { 77 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES); 78 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER); 79 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT); 80 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities", 81 GAL_CONTACT_WITH_ID); 82 sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER); 83 } 84 85 @Override 86 public boolean onCreate() { 87 return true; 88 } 89 90 static class GalProjection { 91 final int size; 92 final HashMap<String, Integer> columnMap = new HashMap<String, Integer>(); 93 94 GalProjection(String[] projection) { 95 size = projection.length; 96 for (int i = 0; i < projection.length; i++) { 97 columnMap.put(projection[i], i); 98 } 99 } 100 } 101 102 static class GalContactRow { 103 private final GalProjection mProjection; 104 private Object[] row; 105 static long dataId = 1; 106 107 GalContactRow(GalProjection projection, long contactId, String lookupKey, 108 String accountName, String displayName) { 109 this.mProjection = projection; 110 row = new Object[projection.size]; 111 112 put(Contacts.Entity.CONTACT_ID, contactId); 113 114 // We only have one raw contact per aggregate, so they can have the same ID 115 put(Contacts.Entity.RAW_CONTACT_ID, contactId); 116 put(Contacts.Entity.DATA_ID, dataId++); 117 118 put(Contacts.DISPLAY_NAME, displayName); 119 120 // TODO alternative display name 121 put(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName); 122 123 put(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 124 put(RawContacts.ACCOUNT_NAME, accountName); 125 put(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1); 126 put(Data.IS_READ_ONLY, 1); 127 } 128 129 Object[] getRow () { 130 return row; 131 } 132 133 void put(String columnName, Object value) { 134 Integer integer = mProjection.columnMap.get(columnName); 135 if (integer != null) { 136 row[integer] = value; 137 } else { 138 System.out.println("Unsupported column: " + columnName); 139 } 140 } 141 142 static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection, 143 long contactId, String lookupKey, String accountName, String displayName, 144 String address) { 145 if (!TextUtils.isEmpty(address)) { 146 GalContactRow r = new GalContactRow( 147 galProjection, contactId, lookupKey, accountName, displayName); 148 r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); 149 r.put(Email.TYPE, Email.TYPE_WORK); 150 r.put(Email.ADDRESS, address); 151 cursor.addRow(r.getRow()); 152 } 153 } 154 155 static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId, 156 String lookupKey, String accountName, String displayName, int type, String number) { 157 if (!TextUtils.isEmpty(number)) { 158 GalContactRow r = new GalContactRow( 159 projection, contactId, lookupKey, accountName, displayName); 160 r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); 161 r.put(Phone.TYPE, type); 162 r.put(Phone.NUMBER, number); 163 cursor.addRow(r.getRow()); 164 } 165 } 166 167 public static void addNameRow(MatrixCursor cursor, GalProjection galProjection, 168 long contactId, String lookupKey, String accountName, String displayName, 169 String firstName, String lastName) { 170 GalContactRow r = new GalContactRow( 171 galProjection, contactId, lookupKey, accountName, displayName); 172 r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); 173 r.put(StructuredName.GIVEN_NAME, firstName); 174 r.put(StructuredName.FAMILY_NAME, lastName); 175 r.put(StructuredName.DISPLAY_NAME, displayName); 176 cursor.addRow(r.getRow()); 177 } 178 } 179 180 /** 181 * Find the record id of an Account, given its name (email address) 182 * @param accountName the name of the account 183 * @return the record id of the Account, or -1 if not found 184 */ 185 /*package*/ long getAccountIdByName(Context context, String accountName) { 186 Long accountId = mAccountIdMap.get(accountName); 187 if (accountId == null) { 188 accountId = Utility.getFirstRowLong(context, Account.CONTENT_URI, 189 EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", 190 new String[] {accountName}, null, EmailContent.ID_PROJECTION_COLUMN , -1L); 191 if (accountId != -1) { 192 mAccountIdMap.put(accountName, accountId); 193 } 194 } 195 return accountId; 196 } 197 198 @Override 199 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 200 String sortOrder) { 201 int match = sURIMatcher.match(uri); 202 MatrixCursor cursor; 203 Object[] row; 204 PackedString ps; 205 String lookupKey; 206 207 switch (match) { 208 case GAL_DIRECTORIES: { 209 // Assuming that GAL can be used with all exchange accounts 210 android.accounts.Account[] accounts = AccountManager.get(getContext()) 211 .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 212 cursor = new MatrixCursor(projection); 213 if (accounts != null) { 214 for (android.accounts.Account account : accounts) { 215 row = new Object[projection.length]; 216 217 for (int i = 0; i < projection.length; i++) { 218 String column = projection[i]; 219 if (column.equals(Directory.ACCOUNT_NAME)) { 220 row[i] = account.name; 221 } else if (column.equals(Directory.ACCOUNT_TYPE)) { 222 row[i] = account.type; 223 } else if (column.equals(Directory.TYPE_RESOURCE_ID)) { 224 Bundle bundle = null; 225 String accountType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE; 226 bundle = new AccountServiceProxy(getContext()) 227 .getConfigurationData(accountType); 228 // Default to the alternative name, erring on the conservative side 229 int exchangeName = R.string.exchange_name_alternate; 230 if (bundle != null && !bundle.getBoolean( 231 Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS, 232 true)) { 233 exchangeName = R.string.exchange_name; 234 } 235 row[i] = exchangeName; 236 } else if (column.equals(Directory.DISPLAY_NAME)) { 237 // If the account name is an email address, extract 238 // the domain name and use it as the directory display name 239 final String accountName = account.name; 240 int atIndex = accountName.indexOf('@'); 241 if (atIndex != -1 && atIndex < accountName.length() - 2) { 242 final char firstLetter = Character.toUpperCase( 243 accountName.charAt(atIndex + 1)); 244 row[i] = firstLetter + accountName.substring(atIndex + 2); 245 } else { 246 row[i] = account.name; 247 } 248 } else if (column.equals(Directory.EXPORT_SUPPORT)) { 249 row[i] = Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY; 250 } else if (column.equals(Directory.SHORTCUT_SUPPORT)) { 251 row[i] = Directory.SHORTCUT_SUPPORT_NONE; 252 } 253 } 254 cursor.addRow(row); 255 } 256 } 257 return cursor; 258 } 259 260 case GAL_FILTER: 261 case GAL_EMAIL_FILTER: { 262 String filter = uri.getLastPathSegment(); 263 // We should have at least two characters before doing a GAL search 264 if (filter == null || filter.length() < 2) { 265 return null; 266 } 267 268 String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 269 if (accountName == null) { 270 return null; 271 } 272 273 // Enforce a limit on the number of lookup responses 274 String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY); 275 int limit = DEFAULT_LOOKUP_LIMIT; 276 if (limitString != null) { 277 try { 278 limit = Integer.parseInt(limitString); 279 } catch (NumberFormatException e) { 280 limit = 0; 281 } 282 if (limit <= 0) { 283 throw new IllegalArgumentException("Limit not valid: " + limitString); 284 } 285 } 286 287 long callingId = Binder.clearCallingIdentity(); 288 try { 289 // Find the account id to pass along to EasSyncService 290 long accountId = getAccountIdByName(getContext(), accountName); 291 if (accountId == -1) { 292 // The account was deleted? 293 return null; 294 } 295 296 // Get results from the Exchange account 297 GalResult galResult = EasSyncService.searchGal(getContext(), accountId, 298 filter, limit); 299 if (galResult != null) { 300 return buildGalResultCursor(projection, galResult); 301 } 302 } finally { 303 Binder.restoreCallingIdentity(callingId); 304 } 305 break; 306 } 307 308 case GAL_CONTACT: 309 case GAL_CONTACT_WITH_ID: { 310 String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 311 if (accountName == null) { 312 return null; 313 } 314 315 GalProjection galProjection = new GalProjection(projection); 316 cursor = new MatrixCursor(projection); 317 // Handle the decomposition of the key into rows suitable for CP2 318 List<String> pathSegments = uri.getPathSegments(); 319 lookupKey = pathSegments.get(2); 320 long contactId = (match == GAL_CONTACT_WITH_ID) 321 ? Long.parseLong(pathSegments.get(3)) 322 : DEFAULT_CONTACT_ID; 323 ps = new PackedString(lookupKey); 324 String displayName = ps.get(GalData.DISPLAY_NAME); 325 GalContactRow.addEmailAddress(cursor, galProjection, contactId, lookupKey, 326 accountName, displayName, ps.get(GalData.EMAIL_ADDRESS)); 327 GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName, 328 displayName, displayName, Phone.TYPE_HOME, ps.get(GalData.HOME_PHONE)); 329 GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName, 330 displayName, displayName, Phone.TYPE_WORK, ps.get(GalData.WORK_PHONE)); 331 GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName, 332 displayName, displayName, Phone.TYPE_MOBILE, ps.get(GalData.MOBILE_PHONE)); 333 GalContactRow.addNameRow(cursor, galProjection, contactId, accountName, displayName, 334 ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME), displayName); 335 return cursor; 336 } 337 } 338 339 return null; 340 } 341 342 /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) { 343 int displayNameIndex = -1; 344 int alternateDisplayNameIndex = -1;; 345 int emailIndex = -1; 346 int idIndex = -1; 347 int lookupIndex = -1; 348 349 for (int i = 0; i < projection.length; i++) { 350 String column = projection[i]; 351 if (Contacts.DISPLAY_NAME.equals(column) || 352 Contacts.DISPLAY_NAME_PRIMARY.equals(column)) { 353 displayNameIndex = i; 354 } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) { 355 alternateDisplayNameIndex = i; 356 } else if (CommonDataKinds.Email.ADDRESS.equals(column)) { 357 emailIndex = i; 358 } else if (Contacts._ID.equals(column)) { 359 idIndex = i; 360 } else if (Contacts.LOOKUP_KEY.equals(column)) { 361 lookupIndex = i; 362 } 363 } 364 365 Object[] row = new Object[projection.length]; 366 367 /* 368 * ContactsProvider will ensure that every request has a non-null projection. 369 */ 370 MatrixCursor cursor = new MatrixCursor(projection); 371 int count = galResult.galData.size(); 372 for (int i = 0; i < count; i++) { 373 GalData galDataRow = galResult.galData.get(i); 374 String firstName = galDataRow.get(GalData.FIRST_NAME); 375 String lastName = galDataRow.get(GalData.LAST_NAME); 376 String displayName = galDataRow.get(GalData.DISPLAY_NAME); 377 // If we don't have a display name, try to create one using first and last name 378 if (displayName == null) { 379 if (firstName != null && lastName != null) { 380 displayName = firstName + " " + lastName; 381 } else if (firstName != null) { 382 displayName = firstName; 383 } else if (lastName != null) { 384 displayName = lastName; 385 } 386 } 387 galDataRow.put(GalData.DISPLAY_NAME, displayName); 388 389 if (displayNameIndex != -1) { 390 row[displayNameIndex] = displayName; 391 } 392 if (alternateDisplayNameIndex != -1) { 393 // Try to create an alternate display name, using first and last name 394 // TODO: Check with Contacts team to make sure we're using this properly 395 if (firstName != null && lastName != null) { 396 row[alternateDisplayNameIndex] = lastName + " " + firstName; 397 } else { 398 row[alternateDisplayNameIndex] = displayName; 399 } 400 } 401 if (emailIndex != -1) { 402 row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS); 403 } 404 if (idIndex != -1) { 405 row[idIndex] = i + 1; // Let's be 1 based 406 } 407 if (lookupIndex != -1) { 408 // We use the packed string as our lookup key; it contains ALL of the gal data 409 // We do this because we are not able to provide a stable id to ContactsProvider 410 row[lookupIndex] = Uri.encode(galDataRow.toPackedString()); 411 } 412 cursor.addRow(row); 413 } 414 return cursor; 415 } 416 417 @Override 418 public String getType(Uri uri) { 419 int match = sURIMatcher.match(uri); 420 switch (match) { 421 case GAL_FILTER: 422 return Contacts.CONTENT_ITEM_TYPE; 423 } 424 return null; 425 } 426 427 @Override 428 public int delete(Uri uri, String selection, String[] selectionArgs) { 429 throw new UnsupportedOperationException(); 430 } 431 432 @Override 433 public Uri insert(Uri uri, ContentValues values) { 434 throw new UnsupportedOperationException(); 435 } 436 437 @Override 438 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 439 throw new UnsupportedOperationException(); 440 } 441 } 442