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