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