Home | History | Annotate | Download | only in provider
      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