/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.exchange.provider; import com.android.emailcommon.Configuration; import com.android.emailcommon.mail.PackedString; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.service.AccountServiceProxy; import com.android.emailcommon.utility.Utility; import com.android.exchange.Eas; import com.android.exchange.EasSyncService; import com.android.exchange.R; import com.android.exchange.provider.GalResult.GalData; import android.accounts.AccountManager; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.RemoteException; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Contacts.Data; import android.provider.ContactsContract.Directory; import android.provider.ContactsContract.RawContacts; import android.text.TextUtils; import java.util.HashMap; import java.util.List; /** * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is * used solely to provide GAL (Global Address Lookup) service to email address adapters */ public class ExchangeDirectoryProvider extends ContentProvider { public static final String EXCHANGE_GAL_AUTHORITY = "com.android.exchange.directory.provider"; private static final int DEFAULT_CONTACT_ID = 1; private static final int DEFAULT_LOOKUP_LIMIT = 20; private static final int GAL_BASE = 0; private static final int GAL_DIRECTORIES = GAL_BASE; private static final int GAL_FILTER = GAL_BASE + 1; private static final int GAL_CONTACT = GAL_BASE + 2; private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3; private static final int GAL_EMAIL_FILTER = GAL_BASE + 4; private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); /*package*/ final HashMap<String, Long> mAccountIdMap = new HashMap<String, Long>(); static { sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES); sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER); sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT); sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities", GAL_CONTACT_WITH_ID); sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER); } @Override public boolean onCreate() { return true; } static class GalProjection { final int size; final HashMap<String, Integer> columnMap = new HashMap<String, Integer>(); GalProjection(String[] projection) { size = projection.length; for (int i = 0; i < projection.length; i++) { columnMap.put(projection[i], i); } } } static class GalContactRow { private final GalProjection mProjection; private Object[] row; static long dataId = 1; GalContactRow(GalProjection projection, long contactId, String lookupKey, String accountName, String displayName) { this.mProjection = projection; row = new Object[projection.size]; put(Contacts.Entity.CONTACT_ID, contactId); // We only have one raw contact per aggregate, so they can have the same ID put(Contacts.Entity.RAW_CONTACT_ID, contactId); put(Contacts.Entity.DATA_ID, dataId++); put(Contacts.DISPLAY_NAME, displayName); // TODO alternative display name put(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName); put(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); put(RawContacts.ACCOUNT_NAME, accountName); put(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1); put(Data.IS_READ_ONLY, 1); } Object[] getRow () { return row; } void put(String columnName, Object value) { Integer integer = mProjection.columnMap.get(columnName); if (integer != null) { row[integer] = value; } else { System.out.println("Unsupported column: " + columnName); } } static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection, long contactId, String lookupKey, String accountName, String displayName, String address) { if (!TextUtils.isEmpty(address)) { GalContactRow r = new GalContactRow( galProjection, contactId, lookupKey, accountName, displayName); r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); r.put(Email.TYPE, Email.TYPE_WORK); r.put(Email.ADDRESS, address); cursor.addRow(r.getRow()); } } static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId, String lookupKey, String accountName, String displayName, int type, String number) { if (!TextUtils.isEmpty(number)) { GalContactRow r = new GalContactRow( projection, contactId, lookupKey, accountName, displayName); r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); r.put(Phone.TYPE, type); r.put(Phone.NUMBER, number); cursor.addRow(r.getRow()); } } public static void addNameRow(MatrixCursor cursor, GalProjection galProjection, long contactId, String lookupKey, String accountName, String displayName, String firstName, String lastName) { GalContactRow r = new GalContactRow( galProjection, contactId, lookupKey, accountName, displayName); r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); r.put(StructuredName.GIVEN_NAME, firstName); r.put(StructuredName.FAMILY_NAME, lastName); r.put(StructuredName.DISPLAY_NAME, displayName); cursor.addRow(r.getRow()); } } /** * Find the record id of an Account, given its name (email address) * @param accountName the name of the account * @return the record id of the Account, or -1 if not found */ /*package*/ long getAccountIdByName(Context context, String accountName) { Long accountId = mAccountIdMap.get(accountName); if (accountId == null) { accountId = Utility.getFirstRowLong(context, Account.CONTENT_URI, EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", new String[] {accountName}, null, EmailContent.ID_PROJECTION_COLUMN , -1L); if (accountId != -1) { mAccountIdMap.put(accountName, accountId); } } return accountId; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { int match = sURIMatcher.match(uri); MatrixCursor cursor; Object[] row; PackedString ps; String lookupKey; switch (match) { case GAL_DIRECTORIES: { // Assuming that GAL can be used with all exchange accounts android.accounts.Account[] accounts = AccountManager.get(getContext()) .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); cursor = new MatrixCursor(projection); if (accounts != null) { for (android.accounts.Account account : accounts) { row = new Object[projection.length]; for (int i = 0; i < projection.length; i++) { String column = projection[i]; if (column.equals(Directory.ACCOUNT_NAME)) { row[i] = account.name; } else if (column.equals(Directory.ACCOUNT_TYPE)) { row[i] = account.type; } else if (column.equals(Directory.TYPE_RESOURCE_ID)) { Bundle bundle = null; String accountType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE; bundle = new AccountServiceProxy(getContext()) .getConfigurationData(accountType); // Default to the alternative name, erring on the conservative side int exchangeName = R.string.exchange_name_alternate; if (bundle != null && !bundle.getBoolean( Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS, true)) { exchangeName = R.string.exchange_name; } row[i] = exchangeName; } else if (column.equals(Directory.DISPLAY_NAME)) { // If the account name is an email address, extract // the domain name and use it as the directory display name final String accountName = account.name; int atIndex = accountName.indexOf('@'); if (atIndex != -1 && atIndex < accountName.length() - 2) { final char firstLetter = Character.toUpperCase( accountName.charAt(atIndex + 1)); row[i] = firstLetter + accountName.substring(atIndex + 2); } else { row[i] = account.name; } } else if (column.equals(Directory.EXPORT_SUPPORT)) { row[i] = Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY; } else if (column.equals(Directory.SHORTCUT_SUPPORT)) { row[i] = Directory.SHORTCUT_SUPPORT_NONE; } } cursor.addRow(row); } } return cursor; } case GAL_FILTER: case GAL_EMAIL_FILTER: { String filter = uri.getLastPathSegment(); // We should have at least two characters before doing a GAL search if (filter == null || filter.length() < 2) { return null; } String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); if (accountName == null) { return null; } // Enforce a limit on the number of lookup responses String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY); int limit = DEFAULT_LOOKUP_LIMIT; if (limitString != null) { try { limit = Integer.parseInt(limitString); } catch (NumberFormatException e) { limit = 0; } if (limit <= 0) { throw new IllegalArgumentException("Limit not valid: " + limitString); } } long callingId = Binder.clearCallingIdentity(); try { // Find the account id to pass along to EasSyncService long accountId = getAccountIdByName(getContext(), accountName); if (accountId == -1) { // The account was deleted? return null; } // Get results from the Exchange account GalResult galResult = EasSyncService.searchGal(getContext(), accountId, filter, limit); if (galResult != null) { return buildGalResultCursor(projection, galResult); } } finally { Binder.restoreCallingIdentity(callingId); } break; } case GAL_CONTACT: case GAL_CONTACT_WITH_ID: { String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); if (accountName == null) { return null; } GalProjection galProjection = new GalProjection(projection); cursor = new MatrixCursor(projection); // Handle the decomposition of the key into rows suitable for CP2 List<String> pathSegments = uri.getPathSegments(); lookupKey = pathSegments.get(2); long contactId = (match == GAL_CONTACT_WITH_ID) ? Long.parseLong(pathSegments.get(3)) : DEFAULT_CONTACT_ID; ps = new PackedString(lookupKey); String displayName = ps.get(GalData.DISPLAY_NAME); GalContactRow.addEmailAddress(cursor, galProjection, contactId, lookupKey, accountName, displayName, ps.get(GalData.EMAIL_ADDRESS)); GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName, displayName, displayName, Phone.TYPE_HOME, ps.get(GalData.HOME_PHONE)); GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName, displayName, displayName, Phone.TYPE_WORK, ps.get(GalData.WORK_PHONE)); GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName, displayName, displayName, Phone.TYPE_MOBILE, ps.get(GalData.MOBILE_PHONE)); GalContactRow.addNameRow(cursor, galProjection, contactId, accountName, displayName, ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME), displayName); return cursor; } } return null; } /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) { int displayNameIndex = -1; int alternateDisplayNameIndex = -1;; int emailIndex = -1; int idIndex = -1; int lookupIndex = -1; for (int i = 0; i < projection.length; i++) { String column = projection[i]; if (Contacts.DISPLAY_NAME.equals(column) || Contacts.DISPLAY_NAME_PRIMARY.equals(column)) { displayNameIndex = i; } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) { alternateDisplayNameIndex = i; } else if (CommonDataKinds.Email.ADDRESS.equals(column)) { emailIndex = i; } else if (Contacts._ID.equals(column)) { idIndex = i; } else if (Contacts.LOOKUP_KEY.equals(column)) { lookupIndex = i; } } Object[] row = new Object[projection.length]; /* * ContactsProvider will ensure that every request has a non-null projection. */ MatrixCursor cursor = new MatrixCursor(projection); int count = galResult.galData.size(); for (int i = 0; i < count; i++) { GalData galDataRow = galResult.galData.get(i); String firstName = galDataRow.get(GalData.FIRST_NAME); String lastName = galDataRow.get(GalData.LAST_NAME); String displayName = galDataRow.get(GalData.DISPLAY_NAME); // If we don't have a display name, try to create one using first and last name if (displayName == null) { if (firstName != null && lastName != null) { displayName = firstName + " " + lastName; } else if (firstName != null) { displayName = firstName; } else if (lastName != null) { displayName = lastName; } } galDataRow.put(GalData.DISPLAY_NAME, displayName); if (displayNameIndex != -1) { row[displayNameIndex] = displayName; } if (alternateDisplayNameIndex != -1) { // Try to create an alternate display name, using first and last name // TODO: Check with Contacts team to make sure we're using this properly if (firstName != null && lastName != null) { row[alternateDisplayNameIndex] = lastName + " " + firstName; } else { row[alternateDisplayNameIndex] = displayName; } } if (emailIndex != -1) { row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS); } if (idIndex != -1) { row[idIndex] = i + 1; // Let's be 1 based } if (lookupIndex != -1) { // We use the packed string as our lookup key; it contains ALL of the gal data // We do this because we are not able to provide a stable id to ContactsProvider row[lookupIndex] = Uri.encode(galDataRow.toPackedString()); } cursor.addRow(row); } return cursor; } @Override public String getType(Uri uri) { int match = sURIMatcher.match(uri); switch (match) { case GAL_FILTER: return Contacts.CONTENT_ITEM_TYPE; } return null; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } @Override public Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException(); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } }