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