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