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