BluetoothPbapVcardManager.java revision 976a53f0f946e589aa2b5204ce3262abcf2afc55
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 android.content.ContentResolver;
37import android.content.Context;
38import android.database.CursorWindowAllocationException;
39import android.database.Cursor;
40import android.database.MatrixCursor;
41import android.net.Uri;
42import android.provider.CallLog;
43import android.provider.ContactsContract;
44import android.provider.CallLog.Calls;
45import android.provider.ContactsContract.CommonDataKinds;
46import android.provider.ContactsContract.Contacts;
47import android.provider.ContactsContract.Data;
48import android.provider.ContactsContract.CommonDataKinds.Phone;
49import android.provider.ContactsContract.PhoneLookup;
50import android.provider.ContactsContract.RawContactsEntity;
51import android.telephony.PhoneNumberUtils;
52import android.text.TextUtils;
53import android.util.Log;
54
55import com.android.bluetooth.R;
56import com.android.vcard.VCardComposer;
57import com.android.vcard.VCardConfig;
58import com.android.vcard.VCardPhoneNumberTranslationCallback;
59
60import java.io.IOException;
61import java.io.OutputStream;
62import java.util.ArrayList;
63import java.util.Collections;
64
65import javax.obex.ServerOperation;
66import javax.obex.Operation;
67import javax.obex.ResponseCodes;
68
69import com.android.bluetooth.Utils;
70import com.android.bluetooth.util.DevicePolicyUtils;
71
72public class BluetoothPbapVcardManager {
73    private static final String TAG = "BluetoothPbapVcardManager";
74
75    private static final boolean V = BluetoothPbapService.VERBOSE;
76
77    private ContentResolver mResolver;
78
79    private Context mContext;
80
81    private static final int PHONE_NUMBER_COLUMN_INDEX = 3;
82
83    static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC";
84
85    static final String[] PHONES_CONTACTS_PROJECTION = new String[] {
86            Phone.CONTACT_ID, // 0
87            Phone.DISPLAY_NAME, // 1
88    };
89
90    static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
91            PhoneLookup._ID, PhoneLookup.DISPLAY_NAME
92    };
93
94    static final int CONTACTS_ID_COLUMN_INDEX = 0;
95
96    static final int CONTACTS_NAME_COLUMN_INDEX = 1;
97
98    // call histories use dynamic handles, and handles should order by date; the
99    // most recently one should be the first handle. In table "calls", _id and
100    // date are consistent in ordering, to implement simply, we sort by _id
101    // here.
102    static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC";
103
104    private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
105
106    public BluetoothPbapVcardManager(final Context context) {
107        mContext = context;
108        mResolver = mContext.getContentResolver();
109    }
110
111    /**
112     * Create an owner vcard from the configured profile
113     * @param vcardType21
114     * @return
115     */
116    private final String getOwnerPhoneNumberVcardFromProfile(final boolean vcardType21, final byte[] filter) {
117        // Currently only support Generic Vcard 2.1 and 3.0
118        int vcardType;
119        if (vcardType21) {
120            vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
121        } else {
122            vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
123        }
124
125        if (!BluetoothPbapConfig.includePhotosInVcard()) {
126            vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
127        }
128
129        return BluetoothPbapUtils.createProfileVCard(mContext, vcardType,filter);
130    }
131
132    public final String getOwnerPhoneNumberVcard(final boolean vcardType21, final byte[] filter) {
133        //Owner vCard enhancement: Use "ME" profile if configured
134        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
135            String vcard = getOwnerPhoneNumberVcardFromProfile(vcardType21, filter);
136            if (vcard != null && vcard.length() != 0) {
137                return vcard;
138            }
139        }
140        //End enhancement
141
142        BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext);
143        String name = BluetoothPbapService.getLocalPhoneName();
144        String number = BluetoothPbapService.getLocalPhoneNum();
145        String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number,
146                vcardType21);
147        return vcard;
148    }
149
150    public final int getPhonebookSize(final int type) {
151        int size;
152        switch (type) {
153            case BluetoothPbapObexServer.ContentType.PHONEBOOK:
154                size = getContactsSize();
155                break;
156            default:
157                size = getCallHistorySize(type);
158                break;
159        }
160        if (V) Log.v(TAG, "getPhonebookSize size = " + size + " type = " + type);
161        return size;
162    }
163
164    public final int getContactsSize() {
165        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
166        Cursor contactCursor = null;
167        try {
168            contactCursor = mResolver.query(myUri, new String[] {Phone.CONTACT_ID},
169                    CLAUSE_ONLY_VISIBLE, null, Phone.CONTACT_ID);
170            if (contactCursor == null) {
171                return 0;
172            }
173            return getDistinctContactIdSize(contactCursor) + 1; // always has the 0.vcf
174        } catch (CursorWindowAllocationException e) {
175            Log.e(TAG, "CursorWindowAllocationException while getting Contacts size");
176        } finally {
177            if (contactCursor != null) {
178                contactCursor.close();
179            }
180        }
181        return 0;
182    }
183
184    public final int getCallHistorySize(final int type) {
185        final Uri myUri = CallLog.Calls.CONTENT_URI;
186        String selection = BluetoothPbapObexServer.createSelectionPara(type);
187        int size = 0;
188        Cursor callCursor = null;
189        try {
190            callCursor = mResolver.query(myUri, null, selection, null,
191                    CallLog.Calls.DEFAULT_SORT_ORDER);
192            if (callCursor != null) {
193                size = callCursor.getCount();
194            }
195        } catch (CursorWindowAllocationException e) {
196            Log.e(TAG, "CursorWindowAllocationException while getting CallHistory size");
197        } finally {
198            if (callCursor != null) {
199                callCursor.close();
200                callCursor = null;
201            }
202        }
203        return size;
204    }
205
206    public final ArrayList<String> loadCallHistoryList(final int type) {
207        final Uri myUri = CallLog.Calls.CONTENT_URI;
208        String selection = BluetoothPbapObexServer.createSelectionPara(type);
209        String[] projection = new String[] {
210                Calls.NUMBER, Calls.CACHED_NAME, Calls.NUMBER_PRESENTATION
211        };
212        final int CALLS_NUMBER_COLUMN_INDEX = 0;
213        final int CALLS_NAME_COLUMN_INDEX = 1;
214        final int CALLS_NUMBER_PRESENTATION_COLUMN_INDEX = 2;
215
216        Cursor callCursor = null;
217        ArrayList<String> list = new ArrayList<String>();
218        try {
219            callCursor = mResolver.query(myUri, projection, selection, null,
220                    CALLLOG_SORT_ORDER);
221            if (callCursor != null) {
222                for (callCursor.moveToFirst(); !callCursor.isAfterLast();
223                        callCursor.moveToNext()) {
224                    String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX);
225                    if (TextUtils.isEmpty(name)) {
226                        // name not found, use number instead
227                        final int numberPresentation = callCursor.getInt(
228                                CALLS_NUMBER_PRESENTATION_COLUMN_INDEX);
229                        if (numberPresentation != Calls.PRESENTATION_ALLOWED) {
230                            name = mContext.getString(R.string.unknownNumber);
231                        } else {
232                            name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX);
233                        }
234                    }
235                    list.add(name);
236                }
237            }
238        } catch (CursorWindowAllocationException e) {
239            Log.e(TAG, "CursorWindowAllocationException while loading CallHistory");
240        } finally {
241            if (callCursor != null) {
242                callCursor.close();
243                callCursor = null;
244            }
245        }
246        return list;
247    }
248
249    public final ArrayList<String> getPhonebookNameList(final int orderByWhat) {
250        ArrayList<String> nameList = new ArrayList<String>();
251        //Owner vCard enhancement. Use "ME" profile if configured
252        String ownerName = null;
253        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
254            ownerName = BluetoothPbapUtils.getProfileName(mContext);
255        }
256        if (ownerName == null || ownerName.length()==0) {
257            ownerName = BluetoothPbapService.getLocalPhoneName();
258        }
259        nameList.add(ownerName);
260        //End enhancement
261
262        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
263        Cursor contactCursor = null;
264        try {
265            contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
266                    null, Phone.CONTACT_ID);
267            if (contactCursor != null) {
268                appendDistinctNameIdList(nameList,
269                        mContext.getString(android.R.string.unknownName),
270                        contactCursor);
271                if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
272                    if (V) Log.v(TAG, "getPhonebookNameList, order by index");
273                    // Do not need to do anything, as we sort it by index already
274                } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
275                    if (V) Log.v(TAG, "getPhonebookNameList, order by alpha");
276                    Collections.sort(nameList);
277                }
278            }
279        } catch (CursorWindowAllocationException e) {
280            Log.e(TAG, "CursorWindowAllocationException while getting Phonebook name list");
281        } finally {
282            if (contactCursor != null) {
283                contactCursor.close();
284                contactCursor = null;
285            }
286        }
287        return nameList;
288    }
289
290    public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) {
291        ArrayList<String> nameList = new ArrayList<String>();
292        ArrayList<String> tempNameList = new ArrayList<String>();
293
294        Cursor contactCursor = null;
295        Uri uri = null;
296        String[] projection = null;
297
298        if (TextUtils.isEmpty(phoneNumber)) {
299            uri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
300            projection = PHONES_CONTACTS_PROJECTION;
301        } else {
302            uri = Uri.withAppendedPath(getPhoneLookupFilterUri(),
303                Uri.encode(phoneNumber));
304            projection = PHONE_LOOKUP_PROJECTION;
305        }
306
307        try {
308            contactCursor = mResolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null,
309                    Phone.CONTACT_ID);
310
311            if (contactCursor != null) {
312                appendDistinctNameIdList(nameList,
313                        mContext.getString(android.R.string.unknownName),
314                        contactCursor);
315                if (V) {
316                    for (String nameIdStr : nameList) {
317                        Log.v(TAG, "got name " + nameIdStr + " by number " + phoneNumber);
318                    }
319                }
320            }
321        } catch (CursorWindowAllocationException e) {
322            Log.e(TAG, "CursorWindowAllocationException while getting contact names");
323        } finally {
324            if (contactCursor != null) {
325                contactCursor.close();
326                contactCursor = null;
327            }
328        }
329        int tempListSize = tempNameList.size();
330        for (int index = 0; index < tempListSize; index++) {
331            String object = tempNameList.get(index);
332            if (!nameList.contains(object))
333                nameList.add(object);
334        }
335
336        return nameList;
337    }
338
339    public final int composeAndSendCallLogVcards(final int type, Operation op,
340            final int startPoint, final int endPoint, final boolean vcardType21,
341            boolean ignorefilter, byte[] filter) {
342        if (startPoint < 1 || startPoint > endPoint) {
343            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
344            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
345        }
346        String typeSelection = BluetoothPbapObexServer.createSelectionPara(type);
347
348        final Uri myUri = CallLog.Calls.CONTENT_URI;
349        final String[] CALLLOG_PROJECTION = new String[] {
350            CallLog.Calls._ID, // 0
351        };
352        final int ID_COLUMN_INDEX = 0;
353
354        Cursor callsCursor = null;
355        long startPointId = 0;
356        long endPointId = 0;
357        try {
358            // Need test to see if order by _ID is ok here, or by date?
359            callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null,
360                    CALLLOG_SORT_ORDER);
361            if (callsCursor != null) {
362                callsCursor.moveToPosition(startPoint - 1);
363                startPointId = callsCursor.getLong(ID_COLUMN_INDEX);
364                if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId);
365                if (startPoint == endPoint) {
366                    endPointId = startPointId;
367                } else {
368                    callsCursor.moveToPosition(endPoint - 1);
369                    endPointId = callsCursor.getLong(ID_COLUMN_INDEX);
370                }
371                if (V) Log.v(TAG, "Call log query endPointId = " + endPointId);
372            }
373        } catch (CursorWindowAllocationException e) {
374            Log.e(TAG, "CursorWindowAllocationException while composing calllog vcards");
375        } finally {
376            if (callsCursor != null) {
377                callsCursor.close();
378                callsCursor = null;
379            }
380        }
381
382        String recordSelection;
383        if (startPoint == endPoint) {
384            recordSelection = Calls._ID + "=" + startPointId;
385        } else {
386            // The query to call table is by "_id DESC" order, so change
387            // correspondingly.
388            recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<="
389                    + startPointId;
390        }
391
392        String selection;
393        if (typeSelection == null) {
394            selection = recordSelection;
395        } else {
396            selection = "(" + typeSelection + ") AND (" + recordSelection + ")";
397        }
398
399        if (V) Log.v(TAG, "Call log query selection is: " + selection);
400
401        return composeCallLogsAndSendVCards(op, selection, vcardType21, null, ignorefilter, filter);
402    }
403
404    public final int composeAndSendPhonebookVcards(Operation op, final int startPoint,
405            final int endPoint, final boolean vcardType21, String ownerVCard,
406            boolean ignorefilter, byte[] filter) {
407        if (startPoint < 1 || startPoint > endPoint) {
408            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
409            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
410        }
411
412        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
413        Cursor contactCursor = null;
414        Cursor contactIdCursor = new MatrixCursor(new String[] {
415            Phone.CONTACT_ID
416        });
417        try {
418            contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
419                    null, Phone.CONTACT_ID);
420            if (contactCursor != null) {
421                contactIdCursor = ContactCursorFilter.filterByRange(contactCursor, startPoint,
422                        endPoint);
423            }
424        } catch (CursorWindowAllocationException e) {
425            Log.e(TAG, "CursorWindowAllocationException while composing phonebook vcards");
426        } finally {
427            if (contactCursor != null) {
428                contactCursor.close();
429            }
430        }
431        return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard,
432                ignorefilter, filter);
433    }
434
435    public final int composeAndSendPhonebookOneVcard(Operation op, final int offset,
436            final boolean vcardType21, String ownerVCard, int orderByWhat,
437            boolean ignorefilter, byte[] filter) {
438        if (offset < 1) {
439            Log.e(TAG, "Internal error: offset is not correct.");
440            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
441        }
442        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
443
444        Cursor contactCursor = null;
445        Cursor contactIdCursor = new MatrixCursor(new String[] {
446            Phone.CONTACT_ID
447        });
448        try {
449            contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION,
450                    CLAUSE_ONLY_VISIBLE, null, Phone.CONTACT_ID);
451            contactIdCursor = ContactCursorFilter.filterByOffset(contactCursor, offset);
452
453        } catch (CursorWindowAllocationException e) {
454            Log.e(TAG,
455                    "CursorWindowAllocationException while composing phonebook one vcard");
456        } finally {
457            if (contactCursor != null) {
458                contactCursor.close();
459            }
460        }
461        return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard,
462                ignorefilter, filter);
463    }
464
465    /**
466     * Filter contact cursor by certain condition.
467     */
468    public static final class ContactCursorFilter {
469        /**
470         *
471         * @param contactCursor
472         * @param offset
473         * @return a cursor containing contact id of {@code offset} contact.
474         */
475        public static Cursor filterByOffset(Cursor contactCursor, int offset) {
476            return filterByRange(contactCursor, offset, offset);
477        }
478
479        /**
480         *
481         * @param contactCursor
482         * @param startPoint
483         * @param endPoint
484         * @return a cursor containing contact ids of {@code startPoint}th to {@code endPoint}th
485         * contact.
486         */
487        public static Cursor filterByRange(Cursor contactCursor, int startPoint, int endPoint) {
488            final int contactIdColumn = contactCursor.getColumnIndex(Data.CONTACT_ID);
489            long previousContactId = -1;
490            // As startPoint, endOffset index starts from 1 to n, we set
491            // currentPoint base as 1 not 0
492            int currentOffset = 1;
493            final MatrixCursor contactIdsCursor = new MatrixCursor(new String[]{
494                    Phone.CONTACT_ID
495            });
496            while (contactCursor.moveToNext() && currentOffset <= endPoint) {
497                long currentContactId = contactCursor.getLong(contactIdColumn);
498                if (previousContactId != currentContactId) {
499                    previousContactId = currentContactId;
500                    if (currentOffset >= startPoint) {
501                        contactIdsCursor.addRow(new Long[]{currentContactId});
502                        if (V) Log.v(TAG, "contactIdsCursor.addRow: " + currentContactId);
503                    }
504                    currentOffset++;
505                }
506            }
507            return contactIdsCursor;
508        }
509    }
510
511    /**
512     * Handler enterprise contact id in VCardComposer
513     */
514    private static class EnterpriseRawContactEntitlesInfoCallback implements
515            VCardComposer.RawContactEntitlesInfoCallback {
516        @Override
517        public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) {
518            if (Contacts.isEnterpriseContactId(contactId)) {
519                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI,
520                        contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE);
521            } else {
522                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI, contactId);
523            }
524        }
525    }
526
527    public final int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor,
528            final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) {
529        long timestamp = 0;
530        if (V) timestamp = System.currentTimeMillis();
531
532        VCardComposer composer = null;
533        VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter);
534
535        HandlerForStringBuffer buffer = null;
536        try {
537            // Currently only support Generic Vcard 2.1 and 3.0
538            int vcardType;
539            if (vcardType21) {
540                vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
541            } else {
542                vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
543            }
544            if (!vcardfilter.isPhotoEnabled()) {
545                vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
546            }
547
548            // Enhancement: customize Vcard based on preferences/settings and
549            // input from caller
550            composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null);
551            // End enhancement
552
553            // BT does want PAUSE/WAIT conversion while it doesn't want the
554            // other formatting
555            // done by vCard library by default.
556            composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() {
557                public String onValueReceived(String rawValue, int type, String label,
558                        boolean isPrimary) {
559                    // 'p' and 'w' are the standard characters for pause and
560                    // wait
561                    // (see RFC 3601)
562                    // so use those when exporting phone numbers via vCard.
563                    String numberWithControlSequence = rawValue
564                            .replace(PhoneNumberUtils.PAUSE, 'p').replace(PhoneNumberUtils.WAIT,
565                                    'w');
566                    return numberWithControlSequence;
567                }
568            });
569            buffer = new HandlerForStringBuffer(op, ownerVCard);
570            Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount());
571            if (!composer.initWithCallback(contactIdCursor,
572                    new EnterpriseRawContactEntitlesInfoCallback())
573                    || !buffer.onInit(mContext)) {
574                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
575            }
576
577            while (!composer.isAfterLast()) {
578                if (BluetoothPbapObexServer.sIsAborted) {
579                    ((ServerOperation) op).isAborted = true;
580                    BluetoothPbapObexServer.sIsAborted = false;
581                    break;
582                }
583                String vcard = composer.createOneEntry();
584                if (vcard == null) {
585                    Log.e(TAG,
586                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
587                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
588                }
589                if (V) Log.v(TAG, "vCard from composer: " + vcard);
590
591                vcard = vcardfilter.apply(vcard, vcardType21);
592                vcard = StripTelephoneNumber(vcard);
593
594                if (V) Log.v(TAG, "vCard after cleanup: " + vcard);
595
596                if (!buffer.onEntryCreated(vcard)) {
597                    // onEntryCreate() already emits error.
598                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
599                }
600            }
601        } finally {
602            if (composer != null) {
603                composer.terminate();
604            }
605            if (buffer != null) {
606                buffer.onTerminate();
607            }
608        }
609
610        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
611                    + (System.currentTimeMillis() - timestamp) + " ms");
612
613        return ResponseCodes.OBEX_HTTP_OK;
614    }
615
616    public final int composeCallLogsAndSendVCards(Operation op, final String selection,
617            final boolean vcardType21, String ownerVCard, boolean ignorefilter,
618            byte[] filter) {
619        long timestamp = 0;
620        if (V) timestamp = System.currentTimeMillis();
621
622        BluetoothPbapCallLogComposer composer = null;
623        HandlerForStringBuffer buffer = null;
624        try {
625
626            VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter);
627            composer = new BluetoothPbapCallLogComposer(mContext);
628            buffer = new HandlerForStringBuffer(op, ownerVCard);
629            if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER)
630                    || !buffer.onInit(mContext)) {
631                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
632            }
633
634            while (!composer.isAfterLast()) {
635                if (BluetoothPbapObexServer.sIsAborted) {
636                    ((ServerOperation) op).isAborted = true;
637                    BluetoothPbapObexServer.sIsAborted = false;
638                    break;
639                }
640                String vcard = composer.createOneEntry(vcardType21);
641                if (vcard != null) {
642                    vcard = vcardfilter.apply(vcard, vcardType21);
643                }
644                if (vcard == null) {
645                    Log.e(TAG,
646                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
647                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
648                }
649                if (V) {
650                    Log.v(TAG, "Vcard Entry:");
651                    Log.v(TAG, vcard);
652                }
653
654                buffer.onEntryCreated(vcard);
655            }
656        } finally {
657            if (composer != null) {
658                composer.terminate();
659            }
660            if (buffer != null) {
661                buffer.onTerminate();
662            }
663        }
664
665        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
666                + (System.currentTimeMillis() - timestamp) + " ms");
667        return ResponseCodes.OBEX_HTTP_OK;
668    }
669
670    public String StripTelephoneNumber (String vCard){
671        String attr [] = vCard.split(System.getProperty("line.separator"));
672        String Vcard = "";
673            for (int i=0; i < attr.length; i++) {
674                if(attr[i].startsWith("TEL")) {
675                    attr[i] = attr[i].replace("(", "");
676                    attr[i] = attr[i].replace(")", "");
677                    attr[i] = attr[i].replace("-", "");
678                    attr[i] = attr[i].replace(" ", "");
679                }
680            }
681
682            for (int i=0; i < attr.length; i++) {
683                if(!attr[i].equals("")){
684                    Vcard = Vcard.concat(attr[i] + "\n");
685                }
686            }
687        if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard);
688        return Vcard;
689    }
690
691    /**
692     * Handler to emit vCards to PCE.
693     */
694    public class HandlerForStringBuffer {
695        private Operation operation;
696
697        private OutputStream outputStream;
698
699        private String phoneOwnVCard = null;
700
701        public HandlerForStringBuffer(Operation op, String ownerVCard) {
702            operation = op;
703            if (ownerVCard != null) {
704                phoneOwnVCard = ownerVCard;
705                if (V) Log.v(TAG, "phone own number vcard:");
706                if (V) Log.v(TAG, phoneOwnVCard);
707            }
708        }
709
710        private boolean write(String vCard) {
711            try {
712                if (vCard != null) {
713                    outputStream.write(vCard.getBytes());
714                    return true;
715                }
716            } catch (IOException e) {
717                Log.e(TAG, "write outputstrem failed" + e.toString());
718            }
719            return false;
720        }
721
722        public boolean onInit(Context context) {
723            try {
724                outputStream = operation.openOutputStream();
725                if (phoneOwnVCard != null) {
726                    return write(phoneOwnVCard);
727                }
728                return true;
729            } catch (IOException e) {
730                Log.e(TAG, "open outputstrem failed" + e.toString());
731            }
732            return false;
733        }
734
735        public boolean onEntryCreated(String vcard) {
736            return write(vcard);
737        }
738
739        public void onTerminate() {
740            if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
741                if (V) Log.v(TAG, "CloseStream failed!");
742            } else {
743                if (V) Log.v(TAG, "CloseStream ok!");
744            }
745        }
746    }
747
748    public static class VCardFilter {
749        private static enum FilterBit {
750            //       bit  property    onlyCheckV21  excludeForV21
751            FN (       1, "FN",       true,         false),
752            PHOTO(     3, "PHOTO",    false,        false),
753            BDAY(      4, "BDAY",     false,        false),
754            ADR(       5, "ADR",      false,        false),
755            EMAIL(     8, "EMAIL",    false,        false),
756            TITLE(    12, "TITLE",    false,        false),
757            ORG(      16, "ORG",      false,        false),
758            NOTE(     17, "NOTE",     false,        false),
759            URL(      20, "URL",      false,        false),
760            NICKNAME( 23, "NICKNAME", false,        true),
761            DATETIME( 28, "DATETIME", false,        true);
762
763            public final int pos;
764            public final String prop;
765            public final boolean onlyCheckV21;
766            public final boolean excludeForV21;
767
768            FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) {
769                this.pos = pos;
770                this.prop = prop;
771                this.onlyCheckV21 = onlyCheckV21;
772                this.excludeForV21 = excludeForV21;
773            }
774        }
775
776        private static final String SEPARATOR = System.getProperty("line.separator");
777        private final byte[] filter;
778
779        //This function returns true if the attributes needs to be included in the filtered vcard.
780        private boolean isFilteredIn(FilterBit bit, boolean vCardType21) {
781            final int offset = (bit.pos / 8) + 1;
782            final int bit_pos = bit.pos % 8;
783            if (!vCardType21 && bit.onlyCheckV21) return true;
784            if (vCardType21 && bit.excludeForV21) return false;
785            if (filter == null || offset >= filter.length) return true;
786            return ((filter[filter.length - offset] >> bit_pos) & 0x01) != 0;
787        }
788
789        VCardFilter(byte[] filter) {
790            this.filter = filter;
791        }
792
793        public boolean isPhotoEnabled() {
794            return isFilteredIn(FilterBit.PHOTO, false);
795        }
796
797        public String apply(String vCard, boolean vCardType21){
798            if (filter == null) return vCard;
799            String lines[] = vCard.split(SEPARATOR);
800            StringBuilder filteredVCard = new StringBuilder();
801            boolean filteredIn = false;
802
803            for (String line : lines) {
804                // Check whether the current property is changing (ignoring multi-line properties)
805                // and determine if the current property is filtered in.
806                if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) {
807                    String currentProp = line.split("[;:]")[0];
808                    filteredIn = true;
809
810                    for (FilterBit bit : FilterBit.values()) {
811                        if (bit.prop.equals(currentProp)) {
812                            filteredIn = isFilteredIn(bit, vCardType21);
813                            break;
814                        }
815                    }
816
817                    // Since PBAP does not have filter bits for IM and SIP,
818                    // exclude them by default. Easiest way is to exclude all
819                    // X- fields....
820                    if (currentProp.startsWith("X-")) filteredIn = false;
821                }
822
823                // Build filtered vCard
824                if (filteredIn) filteredVCard.append(line + SEPARATOR);
825            }
826
827            return filteredVCard.toString();
828        }
829    }
830
831    private static final Uri getPhoneLookupFilterUri() {
832        return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
833    }
834
835    /**
836     * Get size of the cursor without duplicated contact id. This assumes the
837     * given cursor is sorted by CONATCT_ID.
838     */
839    private static final int getDistinctContactIdSize(Cursor cursor) {
840        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
841        long previousContactId = -1;
842        int count = 0;
843        cursor.moveToPosition(-1);
844        while (cursor.moveToNext()) {
845            final long contactId = cursor.getLong(contactIdColumn);
846            if (previousContactId != contactId) {
847                count++;
848                previousContactId = contactId;
849            }
850        }
851        if (V) {
852            Log.i(TAG, "getDistinctContactIdSize result: " + count);
853        }
854        return count;
855    }
856
857    /**
858     * Append "display_name,contact_id" string array from cursor to ArrayList.
859     * This assumes the given cursor is sorted by CONATCT_ID.
860     */
861    private static void appendDistinctNameIdList(ArrayList<String> resultList,
862            String defaultName, Cursor cursor) {
863        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
864        final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME);
865        long previousContactId = -1;
866        cursor.moveToPosition(-1);
867        while (cursor.moveToNext()) {
868            final long contactId = cursor.getLong(contactIdColumn);
869            String displayName = nameColumn != -1 ? cursor.getString(nameColumn) : defaultName;
870            if (TextUtils.isEmpty(displayName)) {
871                displayName = defaultName;
872            }
873
874            if (previousContactId != contactId) {
875                previousContactId = contactId;
876                resultList.add(displayName + "," + contactId);
877            }
878        }
879        if (V) {
880            for (String nameId : resultList) {
881                Log.i(TAG, "appendDistinctNameIdList result: " + nameId);
882            }
883        }
884    }
885}
886