BluetoothPbapVcardManager.java revision d799e680c488c802ccad4b3eb23df525a5f86b44
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        // By default order is indexed
265        String orderBy = Phone.CONTACT_ID;
266        try {
267            if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
268                orderBy = Phone.DISPLAY_NAME;
269            }
270            contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION,
271                CLAUSE_ONLY_VISIBLE, null, orderBy);
272            if (contactCursor != null) {
273                appendDistinctNameIdList(nameList,
274                        mContext.getString(android.R.string.unknownName),
275                        contactCursor);
276            }
277        } catch (CursorWindowAllocationException e) {
278            Log.e(TAG, "CursorWindowAllocationException while getting phonebook name list");
279        } catch (Exception e) {
280            Log.e(TAG, "Exception while getting phonebook name list", e);
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        // By default order is indexed
449        String orderBy = Phone.CONTACT_ID;
450        try {
451            if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
452                orderBy = Phone.DISPLAY_NAME;
453            }
454            contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION,
455                CLAUSE_ONLY_VISIBLE, null, orderBy);
456        } catch (CursorWindowAllocationException e) {
457            Log.e(TAG,
458                "CursorWindowAllocationException while composing phonebook one vcard");
459        } finally {
460            if (contactCursor != null) {
461                contactIdCursor = ContactCursorFilter.filterByOffset(contactCursor, offset);
462                contactCursor.close();
463                contactCursor = null;
464            }
465        }
466        return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard,
467                ignorefilter, filter);
468    }
469
470    /**
471     * Filter contact cursor by certain condition.
472     */
473    public static final class ContactCursorFilter {
474        /**
475         *
476         * @param contactCursor
477         * @param offset
478         * @return a cursor containing contact id of {@code offset} contact.
479         */
480        public static Cursor filterByOffset(Cursor contactCursor, int offset) {
481            return filterByRange(contactCursor, offset, offset);
482        }
483
484        /**
485         *
486         * @param contactCursor
487         * @param startPoint
488         * @param endPoint
489         * @return a cursor containing contact ids of {@code startPoint}th to {@code endPoint}th
490         * contact.
491         */
492        public static Cursor filterByRange(Cursor contactCursor, int startPoint, int endPoint) {
493            final int contactIdColumn = contactCursor.getColumnIndex(Data.CONTACT_ID);
494            long previousContactId = -1;
495            // As startPoint, endOffset index starts from 1 to n, we set
496            // currentPoint base as 1 not 0
497            int currentOffset = 1;
498            final MatrixCursor contactIdsCursor = new MatrixCursor(new String[]{
499                    Phone.CONTACT_ID
500            });
501            while (contactCursor.moveToNext() && currentOffset <= endPoint) {
502                long currentContactId = contactCursor.getLong(contactIdColumn);
503                if (previousContactId != currentContactId) {
504                    previousContactId = currentContactId;
505                    if (currentOffset >= startPoint) {
506                        contactIdsCursor.addRow(new Long[]{currentContactId});
507                        if (V) Log.v(TAG, "contactIdsCursor.addRow: " + currentContactId);
508                    }
509                    currentOffset++;
510                }
511            }
512            return contactIdsCursor;
513        }
514    }
515
516    /**
517     * Handler enterprise contact id in VCardComposer
518     */
519    private static class EnterpriseRawContactEntitlesInfoCallback implements
520            VCardComposer.RawContactEntitlesInfoCallback {
521        @Override
522        public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) {
523            if (Contacts.isEnterpriseContactId(contactId)) {
524                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI,
525                        contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE);
526            } else {
527                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI, contactId);
528            }
529        }
530    }
531
532    public final int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor,
533            final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) {
534        long timestamp = 0;
535        if (V) timestamp = System.currentTimeMillis();
536
537        VCardComposer composer = null;
538        VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter);
539
540        HandlerForStringBuffer buffer = null;
541        try {
542            // Currently only support Generic Vcard 2.1 and 3.0
543            int vcardType;
544            if (vcardType21) {
545                vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
546            } else {
547                vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
548            }
549            if (!vcardfilter.isPhotoEnabled()) {
550                vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
551            }
552
553            // Enhancement: customize Vcard based on preferences/settings and
554            // input from caller
555            composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null);
556            // End enhancement
557
558            // BT does want PAUSE/WAIT conversion while it doesn't want the
559            // other formatting
560            // done by vCard library by default.
561            composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() {
562                public String onValueReceived(String rawValue, int type, String label,
563                        boolean isPrimary) {
564                    // 'p' and 'w' are the standard characters for pause and
565                    // wait
566                    // (see RFC 3601)
567                    // so use those when exporting phone numbers via vCard.
568                    String numberWithControlSequence = rawValue
569                            .replace(PhoneNumberUtils.PAUSE, 'p').replace(PhoneNumberUtils.WAIT,
570                                    'w');
571                    return numberWithControlSequence;
572                }
573            });
574            buffer = new HandlerForStringBuffer(op, ownerVCard);
575            Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount());
576            if (!composer.initWithCallback(contactIdCursor,
577                    new EnterpriseRawContactEntitlesInfoCallback())
578                    || !buffer.onInit(mContext)) {
579                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
580            }
581
582            while (!composer.isAfterLast()) {
583                if (BluetoothPbapObexServer.sIsAborted) {
584                    ((ServerOperation) op).isAborted = true;
585                    BluetoothPbapObexServer.sIsAborted = false;
586                    break;
587                }
588                String vcard = composer.createOneEntry();
589                if (vcard == null) {
590                    Log.e(TAG,
591                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
592                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
593                }
594                if (V) Log.v(TAG, "vCard from composer: " + vcard);
595
596                vcard = vcardfilter.apply(vcard, vcardType21);
597                vcard = StripTelephoneNumber(vcard);
598
599                if (V) Log.v(TAG, "vCard after cleanup: " + vcard);
600
601                if (!buffer.onEntryCreated(vcard)) {
602                    // onEntryCreate() already emits error.
603                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
604                }
605            }
606        } finally {
607            if (composer != null) {
608                composer.terminate();
609            }
610            if (buffer != null) {
611                buffer.onTerminate();
612            }
613        }
614
615        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
616                    + (System.currentTimeMillis() - timestamp) + " ms");
617
618        return ResponseCodes.OBEX_HTTP_OK;
619    }
620
621    public final int composeCallLogsAndSendVCards(Operation op, final String selection,
622            final boolean vcardType21, String ownerVCard, boolean ignorefilter,
623            byte[] filter) {
624        long timestamp = 0;
625        if (V) timestamp = System.currentTimeMillis();
626
627        BluetoothPbapCallLogComposer composer = null;
628        HandlerForStringBuffer buffer = null;
629        try {
630
631            composer = new BluetoothPbapCallLogComposer(mContext);
632            buffer = new HandlerForStringBuffer(op, ownerVCard);
633            if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER)
634                    || !buffer.onInit(mContext)) {
635                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
636            }
637
638            while (!composer.isAfterLast()) {
639                if (BluetoothPbapObexServer.sIsAborted) {
640                    ((ServerOperation) op).isAborted = true;
641                    BluetoothPbapObexServer.sIsAborted = false;
642                    break;
643                }
644                String vcard = composer.createOneEntry(vcardType21);
645                if (vcard == null) {
646                    Log.e(TAG,
647                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
648                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
649                }
650                if (V) {
651                    Log.v(TAG, "Vcard Entry:");
652                    Log.v(TAG, vcard);
653                }
654
655                buffer.onEntryCreated(vcard);
656            }
657        } finally {
658            if (composer != null) {
659                composer.terminate();
660            }
661            if (buffer != null) {
662                buffer.onTerminate();
663            }
664        }
665
666        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
667                + (System.currentTimeMillis() - timestamp) + " ms");
668        return ResponseCodes.OBEX_HTTP_OK;
669    }
670
671    public String StripTelephoneNumber (String vCard){
672        String attr [] = vCard.split(System.getProperty("line.separator"));
673        String Vcard = "";
674            for (int i=0; i < attr.length; i++) {
675                if(attr[i].startsWith("TEL")) {
676                    attr[i] = attr[i].replace("(", "");
677                    attr[i] = attr[i].replace(")", "");
678                    attr[i] = attr[i].replace("-", "");
679                    attr[i] = attr[i].replace(" ", "");
680                }
681            }
682
683            for (int i=0; i < attr.length; i++) {
684                if(!attr[i].equals("")){
685                    Vcard = Vcard.concat(attr[i] + "\n");
686                }
687            }
688        if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard);
689        return Vcard;
690    }
691
692    /**
693     * Handler to emit vCards to PCE.
694     */
695    public class HandlerForStringBuffer {
696        private Operation operation;
697
698        private OutputStream outputStream;
699
700        private String phoneOwnVCard = null;
701
702        public HandlerForStringBuffer(Operation op, String ownerVCard) {
703            operation = op;
704            if (ownerVCard != null) {
705                phoneOwnVCard = ownerVCard;
706                if (V) Log.v(TAG, "phone own number vcard:");
707                if (V) Log.v(TAG, phoneOwnVCard);
708            }
709        }
710
711        private boolean write(String vCard) {
712            try {
713                if (vCard != null) {
714                    outputStream.write(vCard.getBytes());
715                    return true;
716                }
717            } catch (IOException e) {
718                Log.e(TAG, "write outputstrem failed" + e.toString());
719            }
720            return false;
721        }
722
723        public boolean onInit(Context context) {
724            try {
725                outputStream = operation.openOutputStream();
726                if (phoneOwnVCard != null) {
727                    return write(phoneOwnVCard);
728                }
729                return true;
730            } catch (IOException e) {
731                Log.e(TAG, "open outputstrem failed" + e.toString());
732            }
733            return false;
734        }
735
736        public boolean onEntryCreated(String vcard) {
737            return write(vcard);
738        }
739
740        public void onTerminate() {
741            if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
742                if (V) Log.v(TAG, "CloseStream failed!");
743            } else {
744                if (V) Log.v(TAG, "CloseStream ok!");
745            }
746        }
747    }
748
749    public static class VCardFilter {
750        private static enum FilterBit {
751            //       bit  property    onlyCheckV21  excludeForV21
752            FN (       1, "FN",       true,         false),
753            PHOTO(     3, "PHOTO",    false,        false),
754            BDAY(      4, "BDAY",     false,        false),
755            ADR(       5, "ADR",      false,        false),
756            EMAIL(     8, "EMAIL",    false,        false),
757            TITLE(    12, "TITLE",    false,        false),
758            ORG(      16, "ORG",      false,        false),
759            NOTE(     17, "NOTE",     false,        false),
760            URL(      20, "URL",      false,        false),
761            NICKNAME( 23, "NICKNAME", false,        true),
762            DATETIME( 28, "DATETIME", false,        true);
763
764            public final int pos;
765            public final String prop;
766            public final boolean onlyCheckV21;
767            public final boolean excludeForV21;
768
769            FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) {
770                this.pos = pos;
771                this.prop = prop;
772                this.onlyCheckV21 = onlyCheckV21;
773                this.excludeForV21 = excludeForV21;
774            }
775        }
776
777        private static final String SEPARATOR = System.getProperty("line.separator");
778        private final byte[] filter;
779
780        //This function returns true if the attributes needs to be included in the filtered vcard.
781        private boolean isFilteredIn(FilterBit bit, boolean vCardType21) {
782            final int offset = (bit.pos / 8) + 1;
783            final int bit_pos = bit.pos % 8;
784            if (!vCardType21 && bit.onlyCheckV21) return true;
785            if (vCardType21 && bit.excludeForV21) return false;
786            if (filter == null || offset >= filter.length) return true;
787            return ((filter[filter.length - offset] >> bit_pos) & 0x01) != 0;
788        }
789
790        VCardFilter(byte[] filter) {
791            this.filter = filter;
792        }
793
794        public boolean isPhotoEnabled() {
795            return isFilteredIn(FilterBit.PHOTO, false);
796        }
797
798        public String apply(String vCard, boolean vCardType21){
799            if (filter == null) return vCard;
800            String lines[] = vCard.split(SEPARATOR);
801            StringBuilder filteredVCard = new StringBuilder();
802            boolean filteredIn = false;
803
804            for (String line : lines) {
805                // Check whether the current property is changing (ignoring multi-line properties)
806                // and determine if the current property is filtered in.
807                if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) {
808                    String currentProp = line.split("[;:]")[0];
809                    filteredIn = true;
810
811                    for (FilterBit bit : FilterBit.values()) {
812                        if (bit.prop.equals(currentProp)) {
813                            filteredIn = isFilteredIn(bit, vCardType21);
814                            break;
815                        }
816                    }
817
818                    // Since PBAP does not have filter bits for IM and SIP,
819                    // exclude them by default. Easiest way is to exclude all
820                    // X- fields....
821                    if (currentProp.startsWith("X-")) filteredIn = false;
822                }
823
824                // Build filtered vCard
825                if (filteredIn) filteredVCard.append(line + SEPARATOR);
826            }
827
828            return filteredVCard.toString();
829        }
830    }
831
832    private static final Uri getPhoneLookupFilterUri() {
833        return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
834    }
835
836    /**
837     * Get size of the cursor without duplicated contact id. This assumes the
838     * given cursor is sorted by CONTACT_ID.
839     */
840    private static final int getDistinctContactIdSize(Cursor cursor) {
841        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
842        final int idColumn = cursor.getColumnIndex(Data._ID);
843        long previousContactId = -1;
844        int count = 0;
845        cursor.moveToPosition(-1);
846        while (cursor.moveToNext()) {
847            final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn);
848            if (previousContactId != contactId) {
849                count++;
850                previousContactId = contactId;
851            }
852        }
853        if (V) {
854            Log.i(TAG, "getDistinctContactIdSize result: " + count);
855        }
856        return count;
857    }
858
859    /**
860     * Append "display_name,contact_id" string array from cursor to ArrayList.
861     * This assumes the given cursor is sorted by CONTACT_ID.
862     */
863    private static void appendDistinctNameIdList(ArrayList<String> resultList,
864            String defaultName, Cursor cursor) {
865        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
866        final int idColumn = cursor.getColumnIndex(Data._ID);
867        final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME);
868        cursor.moveToPosition(-1);
869        while (cursor.moveToNext()) {
870            final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn);
871            String displayName = nameColumn != -1 ? cursor.getString(nameColumn) : defaultName;
872            if (TextUtils.isEmpty(displayName)) {
873                displayName = defaultName;
874            }
875
876            String newString = displayName + "," + contactId;
877            if (!resultList.contains(newString)) {
878                resultList.add(newString);
879            }
880        }
881        if (V) {
882            for (String nameId : resultList) {
883                Log.i(TAG, "appendDistinctNameIdList result: " + nameId);
884            }
885        }
886    }
887}
888