BluetoothPbapVcardManager.java revision 1bd017d12cf16ecd52fb486722e300790bddeefc
1/*
2 * Copyright (c) 2008-2009, Motorola, Inc.
3 * Copyright (C) 2009-2012, Broadcom Corporation
4 *
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
9 *
10 * - Redistributions of source code must retain the above copyright notice,
11 * this list of conditions and the following disclaimer.
12 *
13 * - Redistributions in binary form must reproduce the above copyright notice,
14 * this list of conditions and the following disclaimer in the documentation
15 * and/or other materials provided with the distribution.
16 *
17 * - Neither the name of the Motorola, Inc. nor the names of its contributors
18 * may be used to endorse or promote products derived from this software
19 * without specific prior written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 * POSSIBILITY OF SUCH DAMAGE.
32 */
33
34package com.android.bluetooth.pbap;
35
36import com.android.bluetooth.R;
37import com.android.bluetooth.util.DevicePolicyUtils;
38import com.android.vcard.VCardComposer;
39import com.android.vcard.VCardConfig;
40import com.android.vcard.VCardPhoneNumberTranslationCallback;
41
42import android.content.ContentResolver;
43import android.content.Context;
44import android.database.Cursor;
45import android.database.CursorWindowAllocationException;
46import android.database.MatrixCursor;
47import android.net.Uri;
48import android.provider.CallLog;
49import android.provider.CallLog.Calls;
50import android.provider.ContactsContract.CommonDataKinds;
51import android.provider.ContactsContract.CommonDataKinds.Phone;
52import android.provider.ContactsContract.Contacts;
53import android.provider.ContactsContract.Data;
54import android.provider.ContactsContract.PhoneLookup;
55import android.provider.ContactsContract.RawContactsEntity;
56import android.telephony.PhoneNumberUtils;
57import android.text.TextUtils;
58import android.util.Log;
59import java.nio.ByteBuffer;
60import java.util.Collections;
61import java.util.Comparator;
62import com.android.bluetooth.R;
63import com.android.vcard.VCardComposer;
64import com.android.vcard.VCardConfig;
65import com.android.vcard.VCardPhoneNumberTranslationCallback;
66
67import java.io.IOException;
68import java.io.OutputStream;
69import java.util.ArrayList;
70
71import javax.obex.Operation;
72import javax.obex.ResponseCodes;
73import javax.obex.ServerOperation;
74
75public class BluetoothPbapVcardManager {
76    private static final String TAG = "BluetoothPbapVcardManager";
77
78    private static final boolean V = BluetoothPbapService.VERBOSE;
79
80    private ContentResolver mResolver;
81
82    private Context mContext;
83
84    private static final int PHONE_NUMBER_COLUMN_INDEX = 3;
85
86    static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC";
87
88    static final String[] PHONES_CONTACTS_PROJECTION = new String[] {
89            Phone.CONTACT_ID, // 0
90            Phone.DISPLAY_NAME, // 1
91    };
92
93    static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
94            PhoneLookup._ID, PhoneLookup.DISPLAY_NAME
95    };
96
97    static final int CONTACTS_ID_COLUMN_INDEX = 0;
98
99    static final int CONTACTS_NAME_COLUMN_INDEX = 1;
100
101    static long sLastFetchedTimeStamp;
102
103    // call histories use dynamic handles, and handles should order by date; the
104    // most recently one should be the first handle. In table "calls", _id and
105    // date are consistent in ordering, to implement simply, we sort by _id
106    // here.
107    static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC";
108
109    private static final int NEED_SEND_BODY = -1;
110
111    public BluetoothPbapVcardManager(final Context context) {
112        mContext = context;
113        mResolver = mContext.getContentResolver();
114        sLastFetchedTimeStamp = System.currentTimeMillis();
115    }
116
117    /**
118     * Create an owner vcard from the configured profile
119     * @param vcardType21
120     * @return
121     */
122    private String getOwnerPhoneNumberVcardFromProfile(
123            final boolean vcardType21, final byte[] filter) {
124        // Currently only support Generic Vcard 2.1 and 3.0
125        int vcardType;
126        if (vcardType21) {
127            vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
128        } else {
129            vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
130        }
131
132        if (!BluetoothPbapConfig.includePhotosInVcard()) {
133            vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
134        }
135
136        return BluetoothPbapUtils.createProfileVCard(mContext, vcardType,filter);
137    }
138
139    public final String getOwnerPhoneNumberVcard(final boolean vcardType21, final byte[] filter) {
140        //Owner vCard enhancement: Use "ME" profile if configured
141        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
142            String vcard = getOwnerPhoneNumberVcardFromProfile(vcardType21, filter);
143            if (vcard != null && vcard.length() != 0) {
144                return vcard;
145            }
146        }
147        //End enhancement
148
149        BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext);
150        String name = BluetoothPbapService.getLocalPhoneName();
151        String number = BluetoothPbapService.getLocalPhoneNum();
152        String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number,
153                vcardType21);
154        return vcard;
155    }
156
157    public final int getPhonebookSize(final int type) {
158        int size;
159        switch (type) {
160            case BluetoothPbapObexServer.ContentType.PHONEBOOK:
161                size = getContactsSize();
162                break;
163            default:
164                size = getCallHistorySize(type);
165                break;
166        }
167        if (V) Log.v(TAG, "getPhonebookSize size = " + size + " type = " + type);
168        return size;
169    }
170
171    public final int getContactsSize() {
172        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
173        Cursor contactCursor = null;
174        try {
175            contactCursor = mResolver.query(
176                    myUri, new String[] {Phone.CONTACT_ID}, null, null, Phone.CONTACT_ID);
177            if (contactCursor == null) {
178                return 0;
179            }
180            return getDistinctContactIdSize(contactCursor) + 1; // always has the 0.vcf
181        } catch (CursorWindowAllocationException e) {
182            Log.e(TAG, "CursorWindowAllocationException while getting Contacts size");
183        } finally {
184            if (contactCursor != null) {
185                contactCursor.close();
186            }
187        }
188        return 0;
189    }
190
191    public final int getCallHistorySize(final int type) {
192        final Uri myUri = CallLog.Calls.CONTENT_URI;
193        String selection = BluetoothPbapObexServer.createSelectionPara(type);
194        int size = 0;
195        Cursor callCursor = null;
196        try {
197            callCursor = mResolver.query(myUri, null, selection, null,
198                    CallLog.Calls.DEFAULT_SORT_ORDER);
199            if (callCursor != null) {
200                size = callCursor.getCount();
201            }
202        } catch (CursorWindowAllocationException e) {
203            Log.e(TAG, "CursorWindowAllocationException while getting CallHistory size");
204        } finally {
205            if (callCursor != null) {
206                callCursor.close();
207                callCursor = null;
208            }
209        }
210        return size;
211    }
212
213    private static final int CALLS_NUMBER_COLUMN_INDEX = 0;
214    private static final int CALLS_NAME_COLUMN_INDEX = 1;
215    private static final int CALLS_NUMBER_PRESENTATION_COLUMN_INDEX = 2;
216    public final ArrayList<String> loadCallHistoryList(final int type) {
217        final Uri myUri = CallLog.Calls.CONTENT_URI;
218        String selection = BluetoothPbapObexServer.createSelectionPara(type);
219        String[] projection = new String[] {
220                Calls.NUMBER, Calls.CACHED_NAME, Calls.NUMBER_PRESENTATION
221        };
222
223
224        Cursor callCursor = null;
225        ArrayList<String> list = new ArrayList<String>();
226        try {
227            callCursor = mResolver.query(myUri, projection, selection, null,
228                    CALLLOG_SORT_ORDER);
229            if (callCursor != null) {
230                for (callCursor.moveToFirst(); !callCursor.isAfterLast();
231                        callCursor.moveToNext()) {
232                    String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX);
233                    if (TextUtils.isEmpty(name)) {
234                        // name not found, use number instead
235                        final int numberPresentation = callCursor.getInt(
236                                CALLS_NUMBER_PRESENTATION_COLUMN_INDEX);
237                        if (numberPresentation != Calls.PRESENTATION_ALLOWED) {
238                            name = mContext.getString(R.string.unknownNumber);
239                        } else {
240                            name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX);
241                        }
242                    }
243                    list.add(name);
244                }
245            }
246        } catch (CursorWindowAllocationException e) {
247            Log.e(TAG, "CursorWindowAllocationException while loading CallHistory");
248        } finally {
249            if (callCursor != null) {
250                callCursor.close();
251                callCursor = null;
252            }
253        }
254        return list;
255    }
256
257    public final ArrayList<String> getPhonebookNameList(final int orderByWhat) {
258        ArrayList<String> nameList = new ArrayList<String>();
259        //Owner vCard enhancement. Use "ME" profile if configured
260        String ownerName = null;
261        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
262            ownerName = BluetoothPbapUtils.getProfileName(mContext);
263        }
264        if (ownerName == null || ownerName.length()==0) {
265            ownerName = BluetoothPbapService.getLocalPhoneName();
266        }
267        nameList.add(ownerName);
268        //End enhancement
269
270        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
271        Cursor contactCursor = null;
272        // By default order is indexed
273        String orderBy = Phone.CONTACT_ID;
274        try {
275            if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
276                orderBy = Phone.DISPLAY_NAME;
277            }
278            contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy);
279            if (contactCursor != null) {
280                appendDistinctNameIdList(nameList,
281                        mContext.getString(android.R.string.unknownName),
282                        contactCursor);
283            }
284        } catch (CursorWindowAllocationException e) {
285            Log.e(TAG, "CursorWindowAllocationException while getting phonebook name list");
286        } catch (Exception e) {
287            Log.e(TAG, "Exception while getting phonebook name list", e);
288        } finally {
289            if (contactCursor != null) {
290                contactCursor.close();
291                contactCursor = null;
292            }
293        }
294        return nameList;
295    }
296
297    final ArrayList<String> getSelectedPhonebookNameList(final int orderByWhat,
298            final boolean vcardType21, int needSendBody, int pbSize, byte[] selector,
299            String vcardselectorop) {
300        ArrayList<String> nameList = new ArrayList<String>();
301        PropertySelector vcardselector = new PropertySelector(selector);
302        VCardComposer composer = null;
303        int vcardType;
304
305        if (vcardType21) {
306            vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
307        } else {
308            vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
309        }
310
311        composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null);
312        composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() {
313
314            @Override
315            public String onValueReceived(
316                    String rawValue, int type, String label, boolean isPrimary) {
317                String numberWithControlSequence = rawValue.replace(PhoneNumberUtils.PAUSE, 'p')
318                                                           .replace(PhoneNumberUtils.WAIT, 'w');
319                return numberWithControlSequence;
320            }
321        });
322
323        // Owner vCard enhancement. Use "ME" profile if configured
324        String ownerName = null;
325        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
326            ownerName = BluetoothPbapUtils.getProfileName(mContext);
327        }
328        if (ownerName == null || ownerName.length() == 0) {
329            ownerName = BluetoothPbapService.getLocalPhoneName();
330        }
331        nameList.add(ownerName);
332        // End enhancement
333
334        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
335        Cursor contactCursor = null;
336        try {
337            contactCursor = mResolver.query(
338                    myUri, PHONES_CONTACTS_PROJECTION, null, null, Phone.CONTACT_ID);
339
340            if (contactCursor != null) {
341                if (!composer.initWithCallback(
342                            contactCursor, new EnterpriseRawContactEntitlesInfoCallback())) {
343                    return nameList;
344                }
345
346                while (!composer.isAfterLast()) {
347                    String vcard = composer.createOneEntry();
348                    if (vcard == null) {
349                        Log.e(TAG, "Failed to read a contact. Error reason: "
350                                        + composer.getErrorReason());
351                        return nameList;
352                    }
353                    if (V) Log.v(TAG, "Checking selected bits in the vcard composer" + vcard);
354
355                    if (!vcardselector.checkVCardSelector(vcard, vcardselectorop)) {
356                        Log.e(TAG, "vcard selector check fail");
357                        vcard = null;
358                        pbSize--;
359                        continue;
360                    } else {
361                        String name = vcardselector.getName(vcard);
362                        if (TextUtils.isEmpty(name)) {
363                            name = mContext.getString(android.R.string.unknownName);
364                        }
365                        nameList.add(name);
366                    }
367                }
368                if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
369                    if (V) Log.v(TAG, "getPhonebookNameList, order by index");
370                    // Do not need to do anything, as we sort it by index already
371                } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
372                    if (V) Log.v(TAG, "getPhonebookNameList, order by alpha");
373                    Collections.sort(nameList);
374                }
375            }
376        } catch (CursorWindowAllocationException e) {
377            Log.e(TAG, "CursorWindowAllocationException while getting Phonebook name list");
378        } finally {
379            if (contactCursor != null) {
380                contactCursor.close();
381                contactCursor = null;
382            }
383        }
384        return nameList;
385    }
386
387    public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) {
388        ArrayList<String> nameList = new ArrayList<String>();
389        ArrayList<String> tempNameList = new ArrayList<String>();
390
391        Cursor contactCursor = null;
392        Uri uri = null;
393        String[] projection = null;
394
395        if (TextUtils.isEmpty(phoneNumber)) {
396            uri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
397            projection = PHONES_CONTACTS_PROJECTION;
398        } else {
399            uri = Uri.withAppendedPath(getPhoneLookupFilterUri(),
400                Uri.encode(phoneNumber));
401            projection = PHONE_LOOKUP_PROJECTION;
402        }
403
404        try {
405            contactCursor = mResolver.query(uri, projection, null, null, Phone.CONTACT_ID);
406
407            if (contactCursor != null) {
408                appendDistinctNameIdList(nameList,
409                        mContext.getString(android.R.string.unknownName),
410                        contactCursor);
411                if (V) {
412                    for (String nameIdStr : nameList) {
413                        Log.v(TAG, "got name " + nameIdStr + " by number " + phoneNumber);
414                    }
415                }
416            }
417        } catch (CursorWindowAllocationException e) {
418            Log.e(TAG, "CursorWindowAllocationException while getting contact names");
419        } finally {
420            if (contactCursor != null) {
421                contactCursor.close();
422                contactCursor = null;
423            }
424        }
425        int tempListSize = tempNameList.size();
426        for (int index = 0; index < tempListSize; index++) {
427            String object = tempNameList.get(index);
428            if (!nameList.contains(object))
429                nameList.add(object);
430        }
431
432        return nameList;
433    }
434
435    byte[] getCallHistoryPrimaryFolderVersion(final int type) {
436        final Uri myUri = CallLog.Calls.CONTENT_URI;
437        String selection = BluetoothPbapObexServer.createSelectionPara(type);
438        selection = selection + " AND date >= " + sLastFetchedTimeStamp;
439
440        Log.d(TAG, "LAST_FETCHED_TIME_STAMP is " + sLastFetchedTimeStamp);
441        Cursor callCursor = null;
442        long count = 0;
443        long primaryVcMsb = 0;
444        ArrayList<String> list = new ArrayList<String>();
445        try {
446            callCursor = mResolver.query(myUri, null, selection, null, null);
447            while (callCursor != null && callCursor.moveToNext()) {
448                count = count + 1;
449            }
450        } catch (Exception e) {
451            Log.e(TAG, "exception while fetching callHistory pvc");
452        } finally {
453            if (callCursor != null) {
454                callCursor.close();
455                callCursor = null;
456            }
457        }
458
459        sLastFetchedTimeStamp = System.currentTimeMillis();
460        Log.d(TAG, "getCallHistoryPrimaryFolderVersion count is " + count + " type is " + type);
461        ByteBuffer pvc = ByteBuffer.allocate(16);
462        pvc.putLong(primaryVcMsb);
463        Log.d(TAG, "primaryVersionCounter is " + BluetoothPbapUtils.sPrimaryVersionCounter);
464        pvc.putLong(count);
465        return pvc.array();
466    }
467
468    private static final String[] CALLLOG_PROJECTION = new String[] {
469            CallLog.Calls._ID, // 0
470    };
471    private static final int ID_COLUMN_INDEX = 0;
472    final int composeAndSendSelectedCallLogVcards(final int type, Operation op,
473            final int startPoint, final int endPoint, final boolean vcardType21, int needSendBody,
474            int pbSize, boolean ignorefilter, byte[] filter, byte[] vcardselector,
475            String vcardselectorop, boolean vcardselect) {
476        if (startPoint < 1 || startPoint > endPoint) {
477            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
478            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
479        }
480        String typeSelection = BluetoothPbapObexServer.createSelectionPara(type);
481
482        final Uri myUri = CallLog.Calls.CONTENT_URI;
483        Cursor callsCursor = null;
484        long startPointId = 0;
485        long endPointId = 0;
486        try {
487            // Need test to see if order by _ID is ok here, or by date?
488            callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null,
489                    CALLLOG_SORT_ORDER);
490            if (callsCursor != null) {
491                callsCursor.moveToPosition(startPoint - 1);
492                startPointId = callsCursor.getLong(ID_COLUMN_INDEX);
493                if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId);
494                if (startPoint == endPoint) {
495                    endPointId = startPointId;
496                } else {
497                    callsCursor.moveToPosition(endPoint - 1);
498                    endPointId = callsCursor.getLong(ID_COLUMN_INDEX);
499                }
500                if (V) Log.v(TAG, "Call log query endPointId = " + endPointId);
501            }
502        } catch (CursorWindowAllocationException e) {
503            Log.e(TAG, "CursorWindowAllocationException while composing calllog vcards");
504        } finally {
505            if (callsCursor != null) {
506                callsCursor.close();
507                callsCursor = null;
508            }
509        }
510
511        String recordSelection;
512        if (startPoint == endPoint) {
513            recordSelection = Calls._ID + "=" + startPointId;
514        } else {
515            // The query to call table is by "_id DESC" order, so change
516            // correspondingly.
517            recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<="
518                    + startPointId;
519        }
520
521        String selection;
522        if (typeSelection == null) {
523            selection = recordSelection;
524        } else {
525            selection = "(" + typeSelection + ") AND (" + recordSelection + ")";
526        }
527
528        if (V) Log.v(TAG, "Call log query selection is: " + selection);
529
530        return composeCallLogsAndSendSelectedVCards(op, selection, vcardType21, needSendBody,
531                pbSize, null, ignorefilter, filter, vcardselector, vcardselectorop, vcardselect);
532    }
533
534    final int composeAndSendPhonebookVcards(Operation op, final int startPoint, final int endPoint,
535            final boolean vcardType21, String ownerVCard, int needSendBody, int pbSize,
536            boolean ignorefilter, byte[] filter, byte[] vcardselector, String vcardselectorop,
537            boolean vcardselect) {
538        if (startPoint < 1 || startPoint > endPoint) {
539            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
540            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
541        }
542
543        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
544        Cursor contactCursor = null;
545        Cursor contactIdCursor = new MatrixCursor(new String[] {
546            Phone.CONTACT_ID
547        });
548        try {
549            contactCursor = mResolver.query(
550                    myUri, PHONES_CONTACTS_PROJECTION, null, null, Phone.CONTACT_ID);
551            if (contactCursor != null) {
552                contactIdCursor = ContactCursorFilter.filterByRange(contactCursor, startPoint,
553                        endPoint);
554            }
555        } catch (CursorWindowAllocationException e) {
556            Log.e(TAG, "CursorWindowAllocationException while composing phonebook vcards");
557        } finally {
558            if (contactCursor != null) {
559                contactCursor.close();
560            }
561        }
562
563        if (vcardselect)
564            return composeContactsAndSendSelectedVCards(op, contactIdCursor, vcardType21,
565                    ownerVCard, needSendBody, pbSize, ignorefilter, filter, vcardselector,
566                    vcardselectorop);
567        else
568            return composeContactsAndSendVCards(
569                    op, contactIdCursor, vcardType21, ownerVCard, ignorefilter, filter);
570    }
571
572    final int composeAndSendPhonebookOneVcard(Operation op, final int offset,
573            final boolean vcardType21, String ownerVCard, int orderByWhat, boolean ignorefilter,
574            byte[] filter) {
575        if (offset < 1) {
576            Log.e(TAG, "Internal error: offset is not correct.");
577            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
578        }
579        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
580
581        Cursor contactCursor = null;
582        Cursor contactIdCursor = new MatrixCursor(new String[] {
583            Phone.CONTACT_ID
584        });
585        // By default order is indexed
586        String orderBy = Phone.CONTACT_ID;
587        try {
588            if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
589                orderBy = Phone.DISPLAY_NAME;
590            }
591            contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy);
592        } catch (CursorWindowAllocationException e) {
593            Log.e(TAG,
594                "CursorWindowAllocationException while composing phonebook one vcard");
595        } finally {
596            if (contactCursor != null) {
597                contactIdCursor = ContactCursorFilter.filterByOffset(contactCursor, offset);
598                contactCursor.close();
599                contactCursor = null;
600            }
601        }
602        return composeContactsAndSendVCards(
603                op, contactIdCursor, vcardType21, ownerVCard, ignorefilter, filter);
604    }
605
606    /**
607     * Filter contact cursor by certain condition.
608     */
609    private static final class ContactCursorFilter {
610        /**
611         *
612         * @param contactCursor
613         * @param offset
614         * @return a cursor containing contact id of {@code offset} contact.
615         */
616        public static Cursor filterByOffset(Cursor contactCursor, int offset) {
617            return filterByRange(contactCursor, offset, offset);
618        }
619
620        /**
621         *
622         * @param contactCursor
623         * @param startPoint
624         * @param endPoint
625         * @return a cursor containing contact ids of {@code startPoint}th to {@code endPoint}th
626         * contact.
627         */
628        public static Cursor filterByRange(Cursor contactCursor, int startPoint, int endPoint) {
629            final int contactIdColumn = contactCursor.getColumnIndex(Data.CONTACT_ID);
630            long previousContactId = -1;
631            // As startPoint, endOffset index starts from 1 to n, we set
632            // currentPoint base as 1 not 0
633            int currentOffset = 1;
634            final MatrixCursor contactIdsCursor = new MatrixCursor(new String[]{
635                    Phone.CONTACT_ID
636            });
637            while (contactCursor.moveToNext() && currentOffset <= endPoint) {
638                long currentContactId = contactCursor.getLong(contactIdColumn);
639                if (previousContactId != currentContactId) {
640                    previousContactId = currentContactId;
641                    if (currentOffset >= startPoint) {
642                        contactIdsCursor.addRow(new Long[]{currentContactId});
643                        if (V) Log.v(TAG, "contactIdsCursor.addRow: " + currentContactId);
644                    }
645                    currentOffset++;
646                }
647            }
648            return contactIdsCursor;
649        }
650    }
651
652    /**
653     * Handler enterprise contact id in VCardComposer
654     */
655    private static class EnterpriseRawContactEntitlesInfoCallback implements
656            VCardComposer.RawContactEntitlesInfoCallback {
657        @Override
658        public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) {
659            if (Contacts.isEnterpriseContactId(contactId)) {
660                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI,
661                        contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE);
662            } else {
663                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI, contactId);
664            }
665        }
666    }
667
668    private int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor,
669            final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) {
670        long timestamp = 0;
671        if (V) timestamp = System.currentTimeMillis();
672
673        VCardComposer composer = null;
674        VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter);
675
676        HandlerForStringBuffer buffer = null;
677        try {
678            // Currently only support Generic Vcard 2.1 and 3.0
679            int vcardType;
680            if (vcardType21) {
681                vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
682            } else {
683                vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
684            }
685            if (!vcardfilter.isPhotoEnabled()) {
686                vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
687            }
688
689            // Enhancement: customize Vcard based on preferences/settings and
690            // input from caller
691            composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null);
692            // End enhancement
693
694            // BT does want PAUSE/WAIT conversion while it doesn't want the
695            // other formatting
696            // done by vCard library by default.
697            composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() {
698                @Override
699                public String onValueReceived(String rawValue, int type, String label,
700                        boolean isPrimary) {
701                    // 'p' and 'w' are the standard characters for pause and
702                    // wait
703                    // (see RFC 3601)
704                    // so use those when exporting phone numbers via vCard.
705                    String numberWithControlSequence = rawValue
706                            .replace(PhoneNumberUtils.PAUSE, 'p').replace(PhoneNumberUtils.WAIT,
707                                    'w');
708                    return numberWithControlSequence;
709                }
710            });
711            buffer = new HandlerForStringBuffer(op, ownerVCard);
712            Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount());
713            if (!composer.initWithCallback(contactIdCursor,
714                    new EnterpriseRawContactEntitlesInfoCallback())
715                    || !buffer.onInit(mContext)) {
716                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
717            }
718
719            while (!composer.isAfterLast()) {
720                if (BluetoothPbapObexServer.sIsAborted) {
721                    ((ServerOperation) op).isAborted = true;
722                    BluetoothPbapObexServer.sIsAborted = false;
723                    break;
724                }
725                String vcard = composer.createOneEntry();
726                if (vcard == null) {
727                    Log.e(TAG,
728                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
729                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
730                }
731                if (V) Log.v(TAG, "vCard from composer: " + vcard);
732
733                vcard = vcardfilter.apply(vcard, vcardType21);
734                vcard = stripTelephoneNumber(vcard);
735
736                if (V) Log.v(TAG, "vCard after cleanup: " + vcard);
737
738                if (!buffer.onEntryCreated(vcard)) {
739                    // onEntryCreate() already emits error.
740                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
741                }
742            }
743        } finally {
744            if (composer != null) {
745                composer.terminate();
746            }
747            if (buffer != null) {
748                buffer.onTerminate();
749            }
750        }
751
752        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
753                    + (System.currentTimeMillis() - timestamp) + " ms");
754
755        return ResponseCodes.OBEX_HTTP_OK;
756    }
757
758    private int composeContactsAndSendSelectedVCards(Operation op,
759            final Cursor contactIdCursor, final boolean vcardType21, String ownerVCard,
760            int needSendBody, int pbSize, boolean ignorefilter, byte[] filter, byte[] selector,
761            String vcardselectorop) {
762        long timestamp = 0;
763        if (V) timestamp = System.currentTimeMillis();
764
765        VCardComposer composer = null;
766        VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter);
767        PropertySelector vcardselector = new PropertySelector(selector);
768
769        HandlerForStringBuffer buffer = null;
770
771        try {
772            // Currently only support Generic Vcard 2.1 and 3.0
773            int vcardType;
774            if (vcardType21) {
775                vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
776            } else {
777                vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
778            }
779            if (!vcardfilter.isPhotoEnabled()) {
780                vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
781            }
782
783            // Enhancement: customize Vcard based on preferences/settings and
784            // input from caller
785            composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null);
786            // End enhancement
787
788            /* BT does want PAUSE/WAIT conversion while it doesn't want the
789             * other formatting done by vCard library by default. */
790            composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() {
791                @Override
792                public String onValueReceived(
793                        String rawValue, int type, String label, boolean isPrimary) {
794                    /* 'p' and 'w' are the standard characters for pause and wait
795                     * (see RFC 3601) so use those when exporting phone numbers via vCard.*/
796                    String numberWithControlSequence = rawValue.replace(PhoneNumberUtils.PAUSE, 'p')
797                                                               .replace(PhoneNumberUtils.WAIT, 'w');
798                    return numberWithControlSequence;
799                }
800            });
801            buffer = new HandlerForStringBuffer(op, ownerVCard);
802            Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount());
803            if (!composer.initWithCallback(
804                        contactIdCursor, new EnterpriseRawContactEntitlesInfoCallback())
805                    || !buffer.onInit(mContext)) {
806                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
807            }
808
809            while (!composer.isAfterLast()) {
810                if (BluetoothPbapObexServer.sIsAborted) {
811                    ((ServerOperation) op).isAborted = true;
812                    BluetoothPbapObexServer.sIsAborted = false;
813                    break;
814                }
815                String vcard = composer.createOneEntry();
816                if (vcard == null) {
817                    Log.e(TAG,
818                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
819                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
820                }
821                if (V) Log.v(TAG, "Checking selected bits in the vcard composer" + vcard);
822
823                if (!vcardselector.checkVCardSelector(vcard, vcardselectorop)) {
824                    Log.e(TAG, "vcard selector check fail");
825                    vcard = null;
826                    pbSize--;
827                    continue;
828                }
829
830                Log.e(TAG, "vcard selector check pass");
831
832                if (needSendBody == NEED_SEND_BODY) {
833                    vcard = vcardfilter.apply(vcard, vcardType21);
834                    vcard = stripTelephoneNumber(vcard);
835
836                    if (V) Log.v(TAG, "vCard after cleanup: " + vcard);
837
838                    if (!buffer.onEntryCreated(vcard)) {
839                        // onEntryCreate() already emits error.
840                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
841                    }
842                }
843            }
844
845            if (needSendBody != NEED_SEND_BODY) return pbSize;
846        } finally {
847            if (composer != null) {
848                composer.terminate();
849            }
850            if (buffer != null) {
851                buffer.onTerminate();
852            }
853        }
854
855        if (V)
856            Log.v(TAG, "Total vcard composing and sending out takes "
857                            + (System.currentTimeMillis() - timestamp) + " ms");
858
859        return ResponseCodes.OBEX_HTTP_OK;
860    }
861
862    private int composeCallLogsAndSendSelectedVCards(Operation op, final String selection,
863            final boolean vcardType21, int needSendBody, int pbSize, String ownerVCard,
864            boolean ignorefilter, byte[] filter, byte[] selector, String vcardselectorop,
865            boolean vCardSelct) {
866        long timestamp = 0;
867        if (V) timestamp = System.currentTimeMillis();
868
869        BluetoothPbapCallLogComposer composer = null;
870        HandlerForStringBuffer buffer = null;
871
872        try {
873            VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter);
874            PropertySelector vcardselector = new PropertySelector(selector);
875            composer = new BluetoothPbapCallLogComposer(mContext);
876            buffer = new HandlerForStringBuffer(op, ownerVCard);
877            if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER)
878                    || !buffer.onInit(mContext)) {
879                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
880            }
881
882            while (!composer.isAfterLast()) {
883                if (BluetoothPbapObexServer.sIsAborted) {
884                    ((ServerOperation) op).isAborted = true;
885                    BluetoothPbapObexServer.sIsAborted = false;
886                    break;
887                }
888                String vcard = composer.createOneEntry(vcardType21);
889                if (vCardSelct) {
890                    if (!vcardselector.checkVCardSelector(vcard, vcardselectorop)) {
891                        Log.e(TAG, "Checking vcard selector for call log");
892                        vcard = null;
893                        pbSize--;
894                        continue;
895                    }
896                    if (needSendBody == NEED_SEND_BODY) {
897                        if (vcard != null) {
898                            vcard = vcardfilter.apply(vcard, vcardType21);
899                        }
900                        if (vcard == null) {
901                            Log.e(TAG, "Failed to read a contact. Error reason: "
902                                            + composer.getErrorReason());
903                            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
904                        }
905                        if (V) {
906                            Log.v(TAG, "Vcard Entry:");
907                            Log.v(TAG, vcard);
908                        }
909                        buffer.onEntryCreated(vcard);
910                    }
911                } else {
912                    if (vcard == null) {
913                        Log.e(TAG, "Failed to read a contact. Error reason: "
914                                        + composer.getErrorReason());
915                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
916                    }
917                    if (V) {
918                        Log.v(TAG, "Vcard Entry:");
919                        Log.v(TAG, vcard);
920                    }
921                    buffer.onEntryCreated(vcard);
922                }
923            }
924            if (needSendBody != NEED_SEND_BODY && vCardSelct) return pbSize;
925        } finally {
926            if (composer != null) {
927                composer.terminate();
928            }
929            if (buffer != null) {
930                buffer.onTerminate();
931            }
932        }
933
934        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
935                + (System.currentTimeMillis() - timestamp) + " ms");
936        return ResponseCodes.OBEX_HTTP_OK;
937    }
938
939    public String stripTelephoneNumber(String vCard){
940        String[] attr = vCard.split(System.getProperty("line.separator"));
941        String stripedVCard = "";
942            for (int i=0; i < attr.length; i++) {
943                if(attr[i].startsWith("TEL")) {
944                    attr[i] = attr[i].replace("(", "");
945                    attr[i] = attr[i].replace(")", "");
946                    attr[i] = attr[i].replace("-", "");
947                    attr[i] = attr[i].replace(" ", "");
948                }
949            }
950
951            for (int i=0; i < attr.length; i++) {
952                if(!attr[i].isEmpty()){
953                    stripedVCard = stripedVCard.concat(attr[i] + "\n");
954                }
955            }
956        if (V) Log.v(TAG, "vCard with stripped telephone no.: " + stripedVCard);
957        return stripedVCard;
958    }
959
960    /**
961     * Handler to emit vCards to PCE.
962     */
963    public class HandlerForStringBuffer {
964        private Operation operation;
965
966        private OutputStream outputStream;
967
968        private String phoneOwnVCard = null;
969
970        public HandlerForStringBuffer(Operation op, String ownerVCard) {
971            operation = op;
972            if (ownerVCard != null) {
973                phoneOwnVCard = ownerVCard;
974                if (V) Log.v(TAG, "phone own number vcard:");
975                if (V) Log.v(TAG, phoneOwnVCard);
976            }
977        }
978
979        private boolean write(String vCard) {
980            try {
981                if (vCard != null) {
982                    outputStream.write(vCard.getBytes());
983                    return true;
984                }
985            } catch (IOException e) {
986                Log.e(TAG, "write outputstrem failed" + e.toString());
987            }
988            return false;
989        }
990
991        public boolean onInit(Context context) {
992            try {
993                outputStream = operation.openOutputStream();
994                if (phoneOwnVCard != null) {
995                    return write(phoneOwnVCard);
996                }
997                return true;
998            } catch (IOException e) {
999                Log.e(TAG, "open outputstrem failed" + e.toString());
1000            }
1001            return false;
1002        }
1003
1004        public boolean onEntryCreated(String vcard) {
1005            return write(vcard);
1006        }
1007
1008        public void onTerminate() {
1009            if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
1010                if (V) Log.v(TAG, "CloseStream failed!");
1011            } else {
1012                if (V) Log.v(TAG, "CloseStream ok!");
1013            }
1014        }
1015    }
1016
1017    public static class VCardFilter {
1018        private static enum FilterBit {
1019            //       bit  property                  onlyCheckV21  excludeForV21
1020            FN (       1, "FN",                       true,         false),
1021            PHOTO(     3, "PHOTO",                    false,        false),
1022            BDAY(      4, "BDAY",                     false,        false),
1023            ADR(       5, "ADR",                      false,        false),
1024            EMAIL(     8, "EMAIL",                    false,        false),
1025            TITLE(    12, "TITLE",                    false,        false),
1026            ORG(      16, "ORG",                      false,        false),
1027            NOTE(     17, "NOTE",                     false,        false),
1028            URL(      20, "URL",                      false,        false),
1029            NICKNAME( 23, "NICKNAME",                 false,        true),
1030            DATETIME( 28, "X-IRMC-CALL-DATETIME",     false,        false);
1031
1032            public final int pos;
1033            public final String prop;
1034            public final boolean onlyCheckV21;
1035            public final boolean excludeForV21;
1036
1037            FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) {
1038                this.pos = pos;
1039                this.prop = prop;
1040                this.onlyCheckV21 = onlyCheckV21;
1041                this.excludeForV21 = excludeForV21;
1042            }
1043        }
1044
1045        private static final String SEPARATOR = System.getProperty("line.separator");
1046        private final byte[] filter;
1047
1048        //This function returns true if the attributes needs to be included in the filtered vcard.
1049        private boolean isFilteredIn(FilterBit bit, boolean vCardType21) {
1050            final int offset = (bit.pos / 8) + 1;
1051            final int bitPos = bit.pos % 8;
1052            if (!vCardType21 && bit.onlyCheckV21) return true;
1053            if (vCardType21 && bit.excludeForV21) return false;
1054            if (filter == null || offset >= filter.length) return true;
1055            return ((filter[filter.length - offset] >> bitPos) & 0x01) != 0;
1056        }
1057
1058        VCardFilter(byte[] filter) {
1059            this.filter = filter;
1060        }
1061
1062        public boolean isPhotoEnabled() {
1063            return isFilteredIn(FilterBit.PHOTO, false);
1064        }
1065
1066        public String apply(String vCard, boolean vCardType21){
1067            if (filter == null) return vCard;
1068            String[] lines = vCard.split(SEPARATOR);
1069            StringBuilder filteredVCard = new StringBuilder();
1070            boolean filteredIn = false;
1071
1072            for (String line : lines) {
1073                // Check whether the current property is changing (ignoring multi-line properties)
1074                // and determine if the current property is filtered in.
1075                if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) {
1076                    String currentProp = line.split("[;:]")[0];
1077                    filteredIn = true;
1078
1079                    for (FilterBit bit : FilterBit.values()) {
1080                        if (bit.prop.equals(currentProp)) {
1081                            filteredIn = isFilteredIn(bit, vCardType21);
1082                            break;
1083                        }
1084                    }
1085
1086                    // Since PBAP does not have filter bits for IM and SIP,
1087                    // exclude them by default. Easiest way is to exclude all
1088                    // X- fields, except date time....
1089                    if (currentProp.startsWith("X-")) {
1090                        filteredIn = false;
1091                        if (currentProp.equals("X-IRMC-CALL-DATETIME")) {
1092                            filteredIn = true;
1093                        }
1094                    }
1095                }
1096
1097                // Build filtered vCard
1098                if (filteredIn) {
1099                    filteredVCard.append(line + SEPARATOR);
1100                }
1101            }
1102
1103            return filteredVCard.toString();
1104        }
1105    }
1106
1107    private static class PropertySelector {
1108        private static enum PropertyMask {
1109            //               bit    property
1110            VERSION(0, "VERSION"),
1111            FN(1, "FN"),
1112            NAME(2, "N"),
1113            PHOTO(3, "PHOTO"),
1114            BDAY(4, "BDAY"),
1115            ADR(5, "ADR"),
1116            LABEL(6, "LABEL"),
1117            TEL(7, "TEL"),
1118            EMAIL(8, "EMAIL"),
1119            TITLE(12, "TITLE"),
1120            ORG(16, "ORG"),
1121            NOTE(17, "NOTE"),
1122            URL(20, "URL"),
1123            NICKNAME(23, "NICKNAME"),
1124            DATETIME(28, "DATETIME");
1125
1126            public final int pos;
1127            public final String prop;
1128
1129            PropertyMask(int pos, String prop) {
1130                this.pos = pos;
1131                this.prop = prop;
1132            }
1133        }
1134
1135        private static final String SEPARATOR = System.getProperty("line.separator");
1136        private final byte[] selector;
1137
1138        PropertySelector(byte[] selector) {
1139            this.selector = selector;
1140        }
1141
1142        private boolean checkbit(int attrBit, byte[] selector) {
1143            int selectorlen = selector.length;
1144            if (((selector[selectorlen - 1 - ((int) attrBit / 8)] >> (attrBit % 8)) & 0x01)
1145                    == 0) {
1146                return false;
1147            }
1148            return true;
1149        }
1150
1151        private boolean checkprop(String vcard, String prop) {
1152            String[] lines = vcard.split(SEPARATOR);
1153            boolean isPresent = false;
1154            for (String line : lines) {
1155                if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) {
1156                    String currentProp = line.split("[;:]")[0];
1157                    if (prop.equals(currentProp)) {
1158                        Log.d(TAG, "bit.prop.equals current prop :" + prop);
1159                        isPresent = true;
1160                        return isPresent;
1161                    }
1162                }
1163            }
1164
1165            return isPresent;
1166        }
1167
1168        private boolean checkVCardSelector(String vcard, String vcardselectorop) {
1169            boolean selectedIn = true;
1170
1171            for (PropertyMask bit : PropertyMask.values()) {
1172                if (checkbit(bit.pos, selector)) {
1173                    Log.d(TAG, "checking for prop :" + bit.prop);
1174                    if (vcardselectorop.equals("0")) {
1175                        if (checkprop(vcard, bit.prop)) {
1176                            Log.d(TAG, "bit.prop.equals current prop :" + bit.prop);
1177                            selectedIn = true;
1178                            break;
1179                        } else {
1180                            selectedIn = false;
1181                        }
1182                    } else if (vcardselectorop.equals("1")) {
1183                        if (!checkprop(vcard, bit.prop)) {
1184                            Log.d(TAG, "bit.prop.notequals current prop" + bit.prop);
1185                            selectedIn = false;
1186                            return selectedIn;
1187                        } else {
1188                            selectedIn = true;
1189                        }
1190                    }
1191                }
1192            }
1193            return selectedIn;
1194        }
1195
1196        private String getName(String vcard) {
1197            String[] lines = vcard.split(SEPARATOR);
1198            String name = "";
1199            for (String line : lines) {
1200                if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) {
1201                    if (line.startsWith("N:"))
1202                        name = line.substring(line.lastIndexOf(':'), line.length());
1203                }
1204            }
1205            Log.d(TAG, "returning name: " + name);
1206            return name;
1207        }
1208    }
1209
1210    private static Uri getPhoneLookupFilterUri() {
1211        return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
1212    }
1213
1214    /**
1215     * Get size of the cursor without duplicated contact id. This assumes the
1216     * given cursor is sorted by CONTACT_ID.
1217     */
1218    private static int getDistinctContactIdSize(Cursor cursor) {
1219        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
1220        final int idColumn = cursor.getColumnIndex(Data._ID);
1221        long previousContactId = -1;
1222        int count = 0;
1223        cursor.moveToPosition(-1);
1224        while (cursor.moveToNext()) {
1225            final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn);
1226            if (previousContactId != contactId) {
1227                count++;
1228                previousContactId = contactId;
1229            }
1230        }
1231        if (V) {
1232            Log.i(TAG, "getDistinctContactIdSize result: " + count);
1233        }
1234        return count;
1235    }
1236
1237    /**
1238     * Append "display_name,contact_id" string array from cursor to ArrayList.
1239     * This assumes the given cursor is sorted by CONTACT_ID.
1240     */
1241    private static void appendDistinctNameIdList(ArrayList<String> resultList,
1242            String defaultName, Cursor cursor) {
1243        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
1244        final int idColumn = cursor.getColumnIndex(Data._ID);
1245        final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME);
1246        cursor.moveToPosition(-1);
1247        while (cursor.moveToNext()) {
1248            final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn);
1249            String displayName = nameColumn != -1 ? cursor.getString(nameColumn) : defaultName;
1250            if (TextUtils.isEmpty(displayName)) {
1251                displayName = defaultName;
1252            }
1253
1254            String newString = displayName + "," + contactId;
1255            if (!resultList.contains(newString)) {
1256                resultList.add(newString);
1257            }
1258        }
1259        if (V) {
1260            for (String nameId : resultList) {
1261                Log.i(TAG, "appendDistinctNameIdList result: " + nameId);
1262            }
1263        }
1264    }
1265}
1266