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