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
17package com.android.exchange.provider;
18
19import com.android.emailcommon.Configuration;
20import com.android.emailcommon.mail.PackedString;
21import com.android.emailcommon.provider.Account;
22import com.android.emailcommon.provider.EmailContent;
23import com.android.emailcommon.provider.EmailContent.AccountColumns;
24import com.android.emailcommon.service.AccountServiceProxy;
25import com.android.emailcommon.utility.Utility;
26import com.android.exchange.Eas;
27import com.android.exchange.EasSyncService;
28import com.android.exchange.R;
29import com.android.exchange.provider.GalResult.GalData;
30
31import android.accounts.AccountManager;
32import android.content.ContentProvider;
33import android.content.ContentValues;
34import android.content.Context;
35import android.content.UriMatcher;
36import android.database.Cursor;
37import android.database.MatrixCursor;
38import android.net.Uri;
39import android.os.Binder;
40import android.os.Bundle;
41import android.os.RemoteException;
42import android.provider.ContactsContract;
43import android.provider.ContactsContract.CommonDataKinds;
44import android.provider.ContactsContract.CommonDataKinds.Email;
45import android.provider.ContactsContract.CommonDataKinds.Phone;
46import android.provider.ContactsContract.CommonDataKinds.StructuredName;
47import android.provider.ContactsContract.Contacts;
48import android.provider.ContactsContract.Contacts.Data;
49import android.provider.ContactsContract.Directory;
50import android.provider.ContactsContract.RawContacts;
51import android.text.TextUtils;
52
53import java.util.HashMap;
54import 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 */
60public 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, displayName, accountName,
334                        displayName, ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME));
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