BluetoothPbapVcardManager.java revision b211df6fcbd7dcd055354b75afc2e68ea49a36d9
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 (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        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            composer = new BluetoothPbapCallLogComposer(mContext);
627            buffer = new HandlerForStringBuffer(op, ownerVCard);
628            if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER)
629                    || !buffer.onInit(mContext)) {
630                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
631            }
632
633            while (!composer.isAfterLast()) {
634                if (BluetoothPbapObexServer.sIsAborted) {
635                    ((ServerOperation) op).isAborted = true;
636                    BluetoothPbapObexServer.sIsAborted = false;
637                    break;
638                }
639                String vcard = composer.createOneEntry(vcardType21);
640                if (vcard == null) {
641                    Log.e(TAG,
642                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
643                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
644                }
645                if (V) {
646                    Log.v(TAG, "Vcard Entry:");
647                    Log.v(TAG, vcard);
648                }
649
650                buffer.onEntryCreated(vcard);
651            }
652        } finally {
653            if (composer != null) {
654                composer.terminate();
655            }
656            if (buffer != null) {
657                buffer.onTerminate();
658            }
659        }
660
661        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
662                + (System.currentTimeMillis() - timestamp) + " ms");
663        return ResponseCodes.OBEX_HTTP_OK;
664    }
665
666    public String StripTelephoneNumber (String vCard){
667        String attr [] = vCard.split(System.getProperty("line.separator"));
668        String Vcard = "";
669            for (int i=0; i < attr.length; i++) {
670                if(attr[i].startsWith("TEL")) {
671                    attr[i] = attr[i].replace("(", "");
672                    attr[i] = attr[i].replace(")", "");
673                    attr[i] = attr[i].replace("-", "");
674                    attr[i] = attr[i].replace(" ", "");
675                }
676            }
677
678            for (int i=0; i < attr.length; i++) {
679                if(!attr[i].equals("")){
680                    Vcard = Vcard.concat(attr[i] + "\n");
681                }
682            }
683        if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard);
684        return Vcard;
685    }
686
687    /**
688     * Handler to emit vCards to PCE.
689     */
690    public class HandlerForStringBuffer {
691        private Operation operation;
692
693        private OutputStream outputStream;
694
695        private String phoneOwnVCard = null;
696
697        public HandlerForStringBuffer(Operation op, String ownerVCard) {
698            operation = op;
699            if (ownerVCard != null) {
700                phoneOwnVCard = ownerVCard;
701                if (V) Log.v(TAG, "phone own number vcard:");
702                if (V) Log.v(TAG, phoneOwnVCard);
703            }
704        }
705
706        private boolean write(String vCard) {
707            try {
708                if (vCard != null) {
709                    outputStream.write(vCard.getBytes());
710                    return true;
711                }
712            } catch (IOException e) {
713                Log.e(TAG, "write outputstrem failed" + e.toString());
714            }
715            return false;
716        }
717
718        public boolean onInit(Context context) {
719            try {
720                outputStream = operation.openOutputStream();
721                if (phoneOwnVCard != null) {
722                    return write(phoneOwnVCard);
723                }
724                return true;
725            } catch (IOException e) {
726                Log.e(TAG, "open outputstrem failed" + e.toString());
727            }
728            return false;
729        }
730
731        public boolean onEntryCreated(String vcard) {
732            return write(vcard);
733        }
734
735        public void onTerminate() {
736            if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
737                if (V) Log.v(TAG, "CloseStream failed!");
738            } else {
739                if (V) Log.v(TAG, "CloseStream ok!");
740            }
741        }
742    }
743
744    public static class VCardFilter {
745        private static enum FilterBit {
746            //       bit  property    onlyCheckV21  excludeForV21
747            FN (       1, "FN",       true,         false),
748            PHOTO(     3, "PHOTO",    false,        false),
749            BDAY(      4, "BDAY",     false,        false),
750            ADR(       5, "ADR",      false,        false),
751            EMAIL(     8, "EMAIL",    false,        false),
752            TITLE(    12, "TITLE",    false,        false),
753            ORG(      16, "ORG",      false,        false),
754            NOTE(     17, "NOTE",     false,        false),
755            URL(      20, "URL",      false,        false),
756            NICKNAME( 23, "NICKNAME", false,        true),
757            DATETIME( 28, "DATETIME", false,        true);
758
759            public final int pos;
760            public final String prop;
761            public final boolean onlyCheckV21;
762            public final boolean excludeForV21;
763
764            FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) {
765                this.pos = pos;
766                this.prop = prop;
767                this.onlyCheckV21 = onlyCheckV21;
768                this.excludeForV21 = excludeForV21;
769            }
770        }
771
772        private static final String SEPARATOR = System.getProperty("line.separator");
773        private final byte[] filter;
774
775        //This function returns true if the attributes needs to be included in the filtered vcard.
776        private boolean isFilteredIn(FilterBit bit, boolean vCardType21) {
777            final int offset = (bit.pos / 8) + 1;
778            final int bit_pos = bit.pos % 8;
779            if (!vCardType21 && bit.onlyCheckV21) return true;
780            if (vCardType21 && bit.excludeForV21) return false;
781            if (filter == null || offset >= filter.length) return true;
782            return ((filter[filter.length - offset] >> bit_pos) & 0x01) != 0;
783        }
784
785        VCardFilter(byte[] filter) {
786            this.filter = filter;
787        }
788
789        public boolean isPhotoEnabled() {
790            return isFilteredIn(FilterBit.PHOTO, false);
791        }
792
793        public String apply(String vCard, boolean vCardType21){
794            if (filter == null) return vCard;
795            String lines[] = vCard.split(SEPARATOR);
796            StringBuilder filteredVCard = new StringBuilder();
797            boolean filteredIn = false;
798
799            for (String line : lines) {
800                // Check whether the current property is changing (ignoring multi-line properties)
801                // and determine if the current property is filtered in.
802                if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) {
803                    String currentProp = line.split("[;:]")[0];
804                    filteredIn = true;
805
806                    for (FilterBit bit : FilterBit.values()) {
807                        if (bit.prop.equals(currentProp)) {
808                            filteredIn = isFilteredIn(bit, vCardType21);
809                            break;
810                        }
811                    }
812
813                    // Since PBAP does not have filter bits for IM and SIP,
814                    // exclude them by default. Easiest way is to exclude all
815                    // X- fields....
816                    if (currentProp.startsWith("X-")) filteredIn = false;
817                }
818
819                // Build filtered vCard
820                if (filteredIn) filteredVCard.append(line + SEPARATOR);
821            }
822
823            return filteredVCard.toString();
824        }
825    }
826
827    private static final Uri getPhoneLookupFilterUri() {
828        return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
829    }
830
831    /**
832     * Get size of the cursor without duplicated contact id. This assumes the
833     * given cursor is sorted by CONATCT_ID.
834     */
835    private static final int getDistinctContactIdSize(Cursor cursor) {
836        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
837        final int idColumn = cursor.getColumnIndex(Data._ID);
838        long previousContactId = -1;
839        int count = 0;
840        cursor.moveToPosition(-1);
841        while (cursor.moveToNext()) {
842            final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn);
843            if (previousContactId != contactId) {
844                count++;
845                previousContactId = contactId;
846            }
847        }
848        if (V) {
849            Log.i(TAG, "getDistinctContactIdSize result: " + count);
850        }
851        return count;
852    }
853
854    /**
855     * Append "display_name,contact_id" string array from cursor to ArrayList.
856     * This assumes the given cursor is sorted by CONATCT_ID.
857     */
858    private static void appendDistinctNameIdList(ArrayList<String> resultList,
859            String defaultName, Cursor cursor) {
860        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
861        final int idColumn = cursor.getColumnIndex(Data._ID);
862        final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME);
863        long previousContactId = -1;
864        cursor.moveToPosition(-1);
865        while (cursor.moveToNext()) {
866            final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn);
867            String displayName = nameColumn != -1 ? cursor.getString(nameColumn) : defaultName;
868            if (TextUtils.isEmpty(displayName)) {
869                displayName = defaultName;
870            }
871
872            if (previousContactId != contactId) {
873                previousContactId = contactId;
874                resultList.add(displayName + "," + contactId);
875            }
876        }
877        if (V) {
878            for (String nameId : resultList) {
879                Log.i(TAG, "appendDistinctNameIdList result: " + nameId);
880            }
881        }
882    }
883}
884