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