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