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