BluetoothPbapVcardManager.java revision 2fbb1d97d08d5d72fe824e543c714e56cd7be236
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            @Override
314            public String onValueReceived(
315                    String rawValue, int type, String label, boolean isPrimary) {
316                String numberWithControlSequence = rawValue.replace(PhoneNumberUtils.PAUSE, 'p')
317                                                           .replace(PhoneNumberUtils.WAIT, 'w');
318                return numberWithControlSequence;
319            }
320        });
321
322        // Owner vCard enhancement. Use "ME" profile if configured
323        String ownerName = null;
324        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
325            ownerName = BluetoothPbapUtils.getProfileName(mContext);
326        }
327        if (ownerName == null || ownerName.length() == 0) {
328            ownerName = BluetoothPbapService.getLocalPhoneName();
329        }
330        nameList.add(ownerName);
331        // End enhancement
332
333        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
334        Cursor contactCursor = null;
335        try {
336            contactCursor = mResolver.query(
337                    myUri, PHONES_CONTACTS_PROJECTION, null, null, Phone.CONTACT_ID);
338
339            if (contactCursor != null) {
340                if (!composer.initWithCallback(
341                            contactCursor, new EnterpriseRawContactEntitlesInfoCallback())) {
342                    return nameList;
343                }
344
345                while (!composer.isAfterLast()) {
346                    String vcard = composer.createOneEntry();
347                    if (vcard == null) {
348                        Log.e(TAG, "Failed to read a contact. Error reason: "
349                                        + composer.getErrorReason());
350                        return nameList;
351                    }
352                    if (V) Log.v(TAG, "Checking selected bits in the vcard composer" + vcard);
353
354                    if (!vcardselector.CheckVcardSelector(vcard, vcardselectorop)) {
355                        Log.e(TAG, "vcard selector check fail");
356                        vcard = null;
357                        pbSize--;
358                        continue;
359                    } else {
360                        String name = vcardselector.getName(vcard);
361                        if (TextUtils.isEmpty(name)) {
362                            name = mContext.getString(android.R.string.unknownName);
363                        }
364                        nameList.add(name);
365                    }
366                }
367                if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
368                    if (V) Log.v(TAG, "getPhonebookNameList, order by index");
369                    // Do not need to do anything, as we sort it by index already
370                } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
371                    if (V) Log.v(TAG, "getPhonebookNameList, order by alpha");
372                    Collections.sort(nameList);
373                }
374            }
375        } catch (CursorWindowAllocationException e) {
376            Log.e(TAG, "CursorWindowAllocationException while getting Phonebook name list");
377        } finally {
378            if (contactCursor != null) {
379                contactCursor.close();
380                contactCursor = null;
381            }
382        }
383        return nameList;
384    }
385
386    public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) {
387        ArrayList<String> nameList = new ArrayList<String>();
388        ArrayList<String> tempNameList = new ArrayList<String>();
389
390        Cursor contactCursor = null;
391        Uri uri = null;
392        String[] projection = null;
393
394        if (TextUtils.isEmpty(phoneNumber)) {
395            uri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
396            projection = PHONES_CONTACTS_PROJECTION;
397        } else {
398            uri = Uri.withAppendedPath(getPhoneLookupFilterUri(),
399                Uri.encode(phoneNumber));
400            projection = PHONE_LOOKUP_PROJECTION;
401        }
402
403        try {
404            contactCursor = mResolver.query(uri, projection, null, null, Phone.CONTACT_ID);
405
406            if (contactCursor != null) {
407                appendDistinctNameIdList(nameList,
408                        mContext.getString(android.R.string.unknownName),
409                        contactCursor);
410                if (V) {
411                    for (String nameIdStr : nameList) {
412                        Log.v(TAG, "got name " + nameIdStr + " by number " + phoneNumber);
413                    }
414                }
415            }
416        } catch (CursorWindowAllocationException e) {
417            Log.e(TAG, "CursorWindowAllocationException while getting contact names");
418        } finally {
419            if (contactCursor != null) {
420                contactCursor.close();
421                contactCursor = null;
422            }
423        }
424        int tempListSize = tempNameList.size();
425        for (int index = 0; index < tempListSize; index++) {
426            String object = tempNameList.get(index);
427            if (!nameList.contains(object))
428                nameList.add(object);
429        }
430
431        return nameList;
432    }
433
434    byte[] getCallHistoryPrimaryFolderVersion(final int type) {
435        final Uri myUri = CallLog.Calls.CONTENT_URI;
436        String selection = BluetoothPbapObexServer.createSelectionPara(type);
437        selection = selection + " AND date >= " + LAST_FETCHED_TIME_STAMP;
438
439        Log.d(TAG, "LAST_FETCHED_TIME_STAMP is " + LAST_FETCHED_TIME_STAMP);
440        Cursor callCursor = null;
441        long count = 0;
442        long primaryVcMsb = 0;
443        ArrayList<String> list = new ArrayList<String>();
444        try {
445            callCursor = mResolver.query(myUri, null, selection, null, null);
446            while (callCursor != null && callCursor.moveToNext()) {
447                count = count + 1;
448            }
449        } catch (Exception e) {
450            Log.e(TAG, "exception while fetching callHistory pvc");
451        } finally {
452            if (callCursor != null) {
453                callCursor.close();
454                callCursor = null;
455            }
456        }
457
458        LAST_FETCHED_TIME_STAMP = System.currentTimeMillis();
459        Log.d(TAG, "getCallHistoryPrimaryFolderVersion count is " + count + " type is " + type);
460        ByteBuffer pvc = ByteBuffer.allocate(16);
461        pvc.putLong(primaryVcMsb);
462        Log.d(TAG, "primaryVersionCounter is " + BluetoothPbapUtils.primaryVersionCounter);
463        pvc.putLong(count);
464        return pvc.array();
465    }
466
467    final int composeAndSendSelectedCallLogVcards(final int type, Operation op,
468            final int startPoint, final int endPoint, final boolean vcardType21, int needSendBody,
469            int pbSize, boolean ignorefilter, byte[] filter, byte[] vcardselector,
470            String vcardselectorop, boolean vcardselect) {
471        if (startPoint < 1 || startPoint > endPoint) {
472            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
473            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
474        }
475        String typeSelection = BluetoothPbapObexServer.createSelectionPara(type);
476
477        final Uri myUri = CallLog.Calls.CONTENT_URI;
478        final String[] CALLLOG_PROJECTION = new String[] {
479            CallLog.Calls._ID, // 0
480        };
481        final int ID_COLUMN_INDEX = 0;
482
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 final 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 final 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 final 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 Vcard = "";
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].equals("")){
953                    Vcard = Vcard.concat(attr[i] + "\n");
954                }
955            }
956        if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard);
957        return Vcard;
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 bit_pos = 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] >> bit_pos) & 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 attr_bit, byte[] selector) {
1143            int selectorlen = selector.length;
1144            if (((selector[selectorlen - 1 - ((int) attr_bit / 8)] >> (attr_bit % 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 final 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 final 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