1/* 2 * Copyright (c) 2008-2009, Motorola, Inc. 3 * Copyright (C) 2009-2012, Broadcom Corporation 4 * 5 * All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions are met: 9 * 10 * - Redistributions of source code must retain the above copyright notice, 11 * this list of conditions and the following disclaimer. 12 * 13 * - Redistributions in binary form must reproduce the above copyright notice, 14 * this list of conditions and the following disclaimer in the documentation 15 * and/or other materials provided with the distribution. 16 * 17 * - Neither the name of the Motorola, Inc. nor the names of its contributors 18 * may be used to endorse or promote products derived from this software 19 * without specific prior written permission. 20 * 21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 24 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 25 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 29 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 * POSSIBILITY OF SUCH DAMAGE. 32 */ 33 34package com.android.bluetooth.pbap; 35 36import android.content.ContentResolver; 37import android.content.Context; 38import android.database.CursorWindowAllocationException; 39import android.database.Cursor; 40import android.database.MatrixCursor; 41import android.net.Uri; 42import android.provider.CallLog; 43import android.provider.ContactsContract; 44import android.provider.CallLog.Calls; 45import android.provider.ContactsContract.CommonDataKinds; 46import android.provider.ContactsContract.Contacts; 47import android.provider.ContactsContract.Data; 48import android.provider.ContactsContract.CommonDataKinds.Phone; 49import android.provider.ContactsContract.PhoneLookup; 50import android.provider.ContactsContract.RawContactsEntity; 51import android.telephony.PhoneNumberUtils; 52import android.text.TextUtils; 53import android.util.Log; 54 55import com.android.bluetooth.R; 56import com.android.vcard.VCardComposer; 57import com.android.vcard.VCardConfig; 58import com.android.vcard.VCardPhoneNumberTranslationCallback; 59 60import java.io.IOException; 61import java.io.OutputStream; 62import java.util.ArrayList; 63import java.util.Collections; 64 65import javax.obex.ServerOperation; 66import javax.obex.Operation; 67import javax.obex.ResponseCodes; 68 69import com.android.bluetooth.Utils; 70import com.android.bluetooth.util.DevicePolicyUtils; 71 72public class BluetoothPbapVcardManager { 73 private static final String TAG = "BluetoothPbapVcardManager"; 74 75 private static final boolean V = BluetoothPbapService.VERBOSE; 76 77 private ContentResolver mResolver; 78 79 private Context mContext; 80 81 private static final int PHONE_NUMBER_COLUMN_INDEX = 3; 82 83 static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC"; 84 85 static final String[] PHONES_CONTACTS_PROJECTION = new String[] { 86 Phone.CONTACT_ID, // 0 87 Phone.DISPLAY_NAME, // 1 88 }; 89 90 static final String[] PHONE_LOOKUP_PROJECTION = new String[] { 91 PhoneLookup._ID, PhoneLookup.DISPLAY_NAME 92 }; 93 94 static final int CONTACTS_ID_COLUMN_INDEX = 0; 95 96 static final int CONTACTS_NAME_COLUMN_INDEX = 1; 97 98 // call histories use dynamic handles, and handles should order by date; the 99 // most recently one should be the first handle. In table "calls", _id and 100 // date are consistent in ordering, to implement simply, we sort by _id 101 // here. 102 static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC"; 103 104 private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; 105 106 public BluetoothPbapVcardManager(final Context context) { 107 mContext = context; 108 mResolver = mContext.getContentResolver(); 109 } 110 111 /** 112 * Create an owner vcard from the configured profile 113 * @param vcardType21 114 * @return 115 */ 116 private final String getOwnerPhoneNumberVcardFromProfile(final boolean vcardType21, final byte[] filter) { 117 // Currently only support Generic Vcard 2.1 and 3.0 118 int vcardType; 119 if (vcardType21) { 120 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 121 } else { 122 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 123 } 124 125 if (!BluetoothPbapConfig.includePhotosInVcard()) { 126 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 127 } 128 129 return BluetoothPbapUtils.createProfileVCard(mContext, vcardType,filter); 130 } 131 132 public final String getOwnerPhoneNumberVcard(final boolean vcardType21, final byte[] filter) { 133 //Owner vCard enhancement: Use "ME" profile if configured 134 if (BluetoothPbapConfig.useProfileForOwnerVcard()) { 135 String vcard = getOwnerPhoneNumberVcardFromProfile(vcardType21, filter); 136 if (vcard != null && vcard.length() != 0) { 137 return vcard; 138 } 139 } 140 //End enhancement 141 142 BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext); 143 String name = BluetoothPbapService.getLocalPhoneName(); 144 String number = BluetoothPbapService.getLocalPhoneNum(); 145 String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number, 146 vcardType21); 147 return vcard; 148 } 149 150 public final int getPhonebookSize(final int type) { 151 int size; 152 switch (type) { 153 case BluetoothPbapObexServer.ContentType.PHONEBOOK: 154 size = getContactsSize(); 155 break; 156 default: 157 size = getCallHistorySize(type); 158 break; 159 } 160 if (V) Log.v(TAG, "getPhonebookSize size = " + size + " type = " + type); 161 return size; 162 } 163 164 public final int getContactsSize() { 165 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 166 Cursor contactCursor = null; 167 try { 168 contactCursor = mResolver.query(myUri, new String[] {Phone.CONTACT_ID}, 169 CLAUSE_ONLY_VISIBLE, null, Phone.CONTACT_ID); 170 if (contactCursor == null) { 171 return 0; 172 } 173 return getDistinctContactIdSize(contactCursor) + 1; // always has the 0.vcf 174 } catch (CursorWindowAllocationException e) { 175 Log.e(TAG, "CursorWindowAllocationException while getting Contacts size"); 176 } finally { 177 if (contactCursor != null) { 178 contactCursor.close(); 179 } 180 } 181 return 0; 182 } 183 184 public final int getCallHistorySize(final int type) { 185 final Uri myUri = CallLog.Calls.CONTENT_URI; 186 String selection = BluetoothPbapObexServer.createSelectionPara(type); 187 int size = 0; 188 Cursor callCursor = null; 189 try { 190 callCursor = mResolver.query(myUri, null, selection, null, 191 CallLog.Calls.DEFAULT_SORT_ORDER); 192 if (callCursor != null) { 193 size = callCursor.getCount(); 194 } 195 } catch (CursorWindowAllocationException e) { 196 Log.e(TAG, "CursorWindowAllocationException while getting CallHistory size"); 197 } finally { 198 if (callCursor != null) { 199 callCursor.close(); 200 callCursor = null; 201 } 202 } 203 return size; 204 } 205 206 public final ArrayList<String> loadCallHistoryList(final int type) { 207 final Uri myUri = CallLog.Calls.CONTENT_URI; 208 String selection = BluetoothPbapObexServer.createSelectionPara(type); 209 String[] projection = new String[] { 210 Calls.NUMBER, Calls.CACHED_NAME, Calls.NUMBER_PRESENTATION 211 }; 212 final int CALLS_NUMBER_COLUMN_INDEX = 0; 213 final int CALLS_NAME_COLUMN_INDEX = 1; 214 final int CALLS_NUMBER_PRESENTATION_COLUMN_INDEX = 2; 215 216 Cursor callCursor = null; 217 ArrayList<String> list = new ArrayList<String>(); 218 try { 219 callCursor = mResolver.query(myUri, projection, selection, null, 220 CALLLOG_SORT_ORDER); 221 if (callCursor != null) { 222 for (callCursor.moveToFirst(); !callCursor.isAfterLast(); 223 callCursor.moveToNext()) { 224 String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX); 225 if (TextUtils.isEmpty(name)) { 226 // name not found, use number instead 227 final int numberPresentation = callCursor.getInt( 228 CALLS_NUMBER_PRESENTATION_COLUMN_INDEX); 229 if (numberPresentation != Calls.PRESENTATION_ALLOWED) { 230 name = mContext.getString(R.string.unknownNumber); 231 } else { 232 name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX); 233 } 234 } 235 list.add(name); 236 } 237 } 238 } catch (CursorWindowAllocationException e) { 239 Log.e(TAG, "CursorWindowAllocationException while loading CallHistory"); 240 } finally { 241 if (callCursor != null) { 242 callCursor.close(); 243 callCursor = null; 244 } 245 } 246 return list; 247 } 248 249 public final ArrayList<String> getPhonebookNameList(final int orderByWhat) { 250 ArrayList<String> nameList = new ArrayList<String>(); 251 //Owner vCard enhancement. Use "ME" profile if configured 252 String ownerName = null; 253 if (BluetoothPbapConfig.useProfileForOwnerVcard()) { 254 ownerName = BluetoothPbapUtils.getProfileName(mContext); 255 } 256 if (ownerName == null || ownerName.length()==0) { 257 ownerName = BluetoothPbapService.getLocalPhoneName(); 258 } 259 nameList.add(ownerName); 260 //End enhancement 261 262 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 263 Cursor contactCursor = null; 264 try { 265 contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 266 null, Phone.CONTACT_ID); 267 if (contactCursor != null) { 268 appendDistinctNameIdList(nameList, 269 mContext.getString(android.R.string.unknownName), 270 contactCursor); 271 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 272 if (V) Log.v(TAG, "getPhonebookNameList, order by index"); 273 // Do not need to do anything, as we sort it by index already 274 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 275 if (V) Log.v(TAG, "getPhonebookNameList, order by alpha"); 276 Collections.sort(nameList); 277 } 278 } 279 } catch (Exception e) { 280 Log.e(TAG, "Exception while getting Phonebook name list", e); 281 } finally { 282 if (contactCursor != null) { 283 contactCursor.close(); 284 contactCursor = null; 285 } 286 } 287 return nameList; 288 } 289 290 public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) { 291 ArrayList<String> nameList = new ArrayList<String>(); 292 ArrayList<String> tempNameList = new ArrayList<String>(); 293 294 Cursor contactCursor = null; 295 Uri uri = null; 296 String[] projection = null; 297 298 if (TextUtils.isEmpty(phoneNumber)) { 299 uri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 300 projection = PHONES_CONTACTS_PROJECTION; 301 } else { 302 uri = Uri.withAppendedPath(getPhoneLookupFilterUri(), 303 Uri.encode(phoneNumber)); 304 projection = PHONE_LOOKUP_PROJECTION; 305 } 306 307 try { 308 contactCursor = mResolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null, 309 Phone.CONTACT_ID); 310 311 if (contactCursor != null) { 312 appendDistinctNameIdList(nameList, 313 mContext.getString(android.R.string.unknownName), 314 contactCursor); 315 if (V) { 316 for (String nameIdStr : nameList) { 317 Log.v(TAG, "got name " + nameIdStr + " by number " + phoneNumber); 318 } 319 } 320 } 321 } catch (CursorWindowAllocationException e) { 322 Log.e(TAG, "CursorWindowAllocationException while getting contact names"); 323 } finally { 324 if (contactCursor != null) { 325 contactCursor.close(); 326 contactCursor = null; 327 } 328 } 329 int tempListSize = tempNameList.size(); 330 for (int index = 0; index < tempListSize; index++) { 331 String object = tempNameList.get(index); 332 if (!nameList.contains(object)) 333 nameList.add(object); 334 } 335 336 return nameList; 337 } 338 339 public final int composeAndSendCallLogVcards(final int type, Operation op, 340 final int startPoint, final int endPoint, final boolean vcardType21, 341 boolean ignorefilter, byte[] filter) { 342 if (startPoint < 1 || startPoint > endPoint) { 343 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 344 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 345 } 346 String typeSelection = BluetoothPbapObexServer.createSelectionPara(type); 347 348 final Uri myUri = CallLog.Calls.CONTENT_URI; 349 final String[] CALLLOG_PROJECTION = new String[] { 350 CallLog.Calls._ID, // 0 351 }; 352 final int ID_COLUMN_INDEX = 0; 353 354 Cursor callsCursor = null; 355 long startPointId = 0; 356 long endPointId = 0; 357 try { 358 // Need test to see if order by _ID is ok here, or by date? 359 callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null, 360 CALLLOG_SORT_ORDER); 361 if (callsCursor != null) { 362 callsCursor.moveToPosition(startPoint - 1); 363 startPointId = callsCursor.getLong(ID_COLUMN_INDEX); 364 if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId); 365 if (startPoint == endPoint) { 366 endPointId = startPointId; 367 } else { 368 callsCursor.moveToPosition(endPoint - 1); 369 endPointId = callsCursor.getLong(ID_COLUMN_INDEX); 370 } 371 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId); 372 } 373 } catch (CursorWindowAllocationException e) { 374 Log.e(TAG, "CursorWindowAllocationException while composing calllog vcards"); 375 } finally { 376 if (callsCursor != null) { 377 callsCursor.close(); 378 callsCursor = null; 379 } 380 } 381 382 String recordSelection; 383 if (startPoint == endPoint) { 384 recordSelection = Calls._ID + "=" + startPointId; 385 } else { 386 // The query to call table is by "_id DESC" order, so change 387 // correspondingly. 388 recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<=" 389 + startPointId; 390 } 391 392 String selection; 393 if (typeSelection == null) { 394 selection = recordSelection; 395 } else { 396 selection = "(" + typeSelection + ") AND (" + recordSelection + ")"; 397 } 398 399 if (V) Log.v(TAG, "Call log query selection is: " + selection); 400 401 return composeCallLogsAndSendVCards(op, selection, vcardType21, null, ignorefilter, filter); 402 } 403 404 public final int composeAndSendPhonebookVcards(Operation op, final int startPoint, 405 final int endPoint, final boolean vcardType21, String ownerVCard, 406 boolean ignorefilter, byte[] filter) { 407 if (startPoint < 1 || startPoint > endPoint) { 408 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 409 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 410 } 411 412 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 413 Cursor contactCursor = null; 414 Cursor contactIdCursor = new MatrixCursor(new String[] { 415 Phone.CONTACT_ID 416 }); 417 try { 418 contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 419 null, Phone.CONTACT_ID); 420 if (contactCursor != null) { 421 contactIdCursor = ContactCursorFilter.filterByRange(contactCursor, startPoint, 422 endPoint); 423 } 424 } catch (CursorWindowAllocationException e) { 425 Log.e(TAG, "CursorWindowAllocationException while composing phonebook vcards"); 426 } finally { 427 if (contactCursor != null) { 428 contactCursor.close(); 429 } 430 } 431 return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard, 432 ignorefilter, filter); 433 } 434 435 public final int composeAndSendPhonebookOneVcard(Operation op, final int offset, 436 final boolean vcardType21, String ownerVCard, int orderByWhat, 437 boolean ignorefilter, byte[] filter) { 438 if (offset < 1) { 439 Log.e(TAG, "Internal error: offset is not correct."); 440 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 441 } 442 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 443 444 Cursor contactCursor = null; 445 Cursor contactIdCursor = new MatrixCursor(new String[] { 446 Phone.CONTACT_ID 447 }); 448 try { 449 contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, 450 CLAUSE_ONLY_VISIBLE, null, Phone.CONTACT_ID); 451 contactIdCursor = ContactCursorFilter.filterByOffset(contactCursor, offset); 452 453 } catch (CursorWindowAllocationException e) { 454 Log.e(TAG, 455 "CursorWindowAllocationException while composing phonebook one vcard"); 456 } finally { 457 if (contactCursor != null) { 458 contactCursor.close(); 459 } 460 } 461 return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard, 462 ignorefilter, filter); 463 } 464 465 /** 466 * Filter contact cursor by certain condition. 467 */ 468 public static final class ContactCursorFilter { 469 /** 470 * 471 * @param contactCursor 472 * @param offset 473 * @return a cursor containing contact id of {@code offset} contact. 474 */ 475 public static Cursor filterByOffset(Cursor contactCursor, int offset) { 476 return filterByRange(contactCursor, offset, offset); 477 } 478 479 /** 480 * 481 * @param contactCursor 482 * @param startPoint 483 * @param endPoint 484 * @return a cursor containing contact ids of {@code startPoint}th to {@code endPoint}th 485 * contact. 486 */ 487 public static Cursor filterByRange(Cursor contactCursor, int startPoint, int endPoint) { 488 final int contactIdColumn = contactCursor.getColumnIndex(Data.CONTACT_ID); 489 long previousContactId = -1; 490 // As startPoint, endOffset index starts from 1 to n, we set 491 // currentPoint base as 1 not 0 492 int currentOffset = 1; 493 final MatrixCursor contactIdsCursor = new MatrixCursor(new String[]{ 494 Phone.CONTACT_ID 495 }); 496 while (contactCursor.moveToNext() && currentOffset <= endPoint) { 497 long currentContactId = contactCursor.getLong(contactIdColumn); 498 if (previousContactId != currentContactId) { 499 previousContactId = currentContactId; 500 if (currentOffset >= startPoint) { 501 contactIdsCursor.addRow(new Long[]{currentContactId}); 502 if (V) Log.v(TAG, "contactIdsCursor.addRow: " + currentContactId); 503 } 504 currentOffset++; 505 } 506 } 507 return contactIdsCursor; 508 } 509 } 510 511 /** 512 * Handler enterprise contact id in VCardComposer 513 */ 514 private static class EnterpriseRawContactEntitlesInfoCallback implements 515 VCardComposer.RawContactEntitlesInfoCallback { 516 @Override 517 public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) { 518 if (Contacts.isEnterpriseContactId(contactId)) { 519 return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI, 520 contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE); 521 } else { 522 return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI, contactId); 523 } 524 } 525 } 526 527 public final int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor, 528 final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) { 529 long timestamp = 0; 530 if (V) timestamp = System.currentTimeMillis(); 531 532 VCardComposer composer = null; 533 VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter); 534 535 HandlerForStringBuffer buffer = null; 536 try { 537 // Currently only support Generic Vcard 2.1 and 3.0 538 int vcardType; 539 if (vcardType21) { 540 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 541 } else { 542 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 543 } 544 if (!vcardfilter.isPhotoEnabled()) { 545 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 546 } 547 548 // Enhancement: customize Vcard based on preferences/settings and 549 // input from caller 550 composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null); 551 // End enhancement 552 553 // BT does want PAUSE/WAIT conversion while it doesn't want the 554 // other formatting 555 // done by vCard library by default. 556 composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() { 557 public String onValueReceived(String rawValue, int type, String label, 558 boolean isPrimary) { 559 // 'p' and 'w' are the standard characters for pause and 560 // wait 561 // (see RFC 3601) 562 // so use those when exporting phone numbers via vCard. 563 String numberWithControlSequence = rawValue 564 .replace(PhoneNumberUtils.PAUSE, 'p').replace(PhoneNumberUtils.WAIT, 565 'w'); 566 return numberWithControlSequence; 567 } 568 }); 569 buffer = new HandlerForStringBuffer(op, ownerVCard); 570 Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount()); 571 if (!composer.initWithCallback(contactIdCursor, 572 new EnterpriseRawContactEntitlesInfoCallback()) 573 || !buffer.onInit(mContext)) { 574 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 575 } 576 577 while (!composer.isAfterLast()) { 578 if (BluetoothPbapObexServer.sIsAborted) { 579 ((ServerOperation) op).isAborted = true; 580 BluetoothPbapObexServer.sIsAborted = false; 581 break; 582 } 583 String vcard = composer.createOneEntry(); 584 if (vcard == null) { 585 Log.e(TAG, 586 "Failed to read a contact. Error reason: " + composer.getErrorReason()); 587 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 588 } 589 if (V) Log.v(TAG, "vCard from composer: " + vcard); 590 591 vcard = vcardfilter.apply(vcard, vcardType21); 592 vcard = StripTelephoneNumber(vcard); 593 594 if (V) Log.v(TAG, "vCard after cleanup: " + vcard); 595 596 if (!buffer.onEntryCreated(vcard)) { 597 // onEntryCreate() already emits error. 598 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 599 } 600 } 601 } finally { 602 if (composer != null) { 603 composer.terminate(); 604 } 605 if (buffer != null) { 606 buffer.onTerminate(); 607 } 608 } 609 610 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 611 + (System.currentTimeMillis() - timestamp) + " ms"); 612 613 return ResponseCodes.OBEX_HTTP_OK; 614 } 615 616 public final int composeCallLogsAndSendVCards(Operation op, final String selection, 617 final boolean vcardType21, String ownerVCard, boolean ignorefilter, 618 byte[] filter) { 619 long timestamp = 0; 620 if (V) timestamp = System.currentTimeMillis(); 621 622 BluetoothPbapCallLogComposer composer = null; 623 HandlerForStringBuffer buffer = null; 624 try { 625 626 VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter); 627 composer = new BluetoothPbapCallLogComposer(mContext); 628 buffer = new HandlerForStringBuffer(op, ownerVCard); 629 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER) 630 || !buffer.onInit(mContext)) { 631 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 632 } 633 634 while (!composer.isAfterLast()) { 635 if (BluetoothPbapObexServer.sIsAborted) { 636 ((ServerOperation) op).isAborted = true; 637 BluetoothPbapObexServer.sIsAborted = false; 638 break; 639 } 640 String vcard = composer.createOneEntry(vcardType21); 641 if (vcard != null) { 642 vcard = vcardfilter.apply(vcard, vcardType21); 643 } 644 if (vcard == null) { 645 Log.e(TAG, 646 "Failed to read a contact. Error reason: " + composer.getErrorReason()); 647 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 648 } 649 if (V) { 650 Log.v(TAG, "Vcard Entry:"); 651 Log.v(TAG, vcard); 652 } 653 654 buffer.onEntryCreated(vcard); 655 } 656 } finally { 657 if (composer != null) { 658 composer.terminate(); 659 } 660 if (buffer != null) { 661 buffer.onTerminate(); 662 } 663 } 664 665 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 666 + (System.currentTimeMillis() - timestamp) + " ms"); 667 return ResponseCodes.OBEX_HTTP_OK; 668 } 669 670 public String StripTelephoneNumber (String vCard){ 671 String attr [] = vCard.split(System.getProperty("line.separator")); 672 String Vcard = ""; 673 for (int i=0; i < attr.length; i++) { 674 if(attr[i].startsWith("TEL")) { 675 attr[i] = attr[i].replace("(", ""); 676 attr[i] = attr[i].replace(")", ""); 677 attr[i] = attr[i].replace("-", ""); 678 attr[i] = attr[i].replace(" ", ""); 679 } 680 } 681 682 for (int i=0; i < attr.length; i++) { 683 if(!attr[i].equals("")){ 684 Vcard = Vcard.concat(attr[i] + "\n"); 685 } 686 } 687 if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard); 688 return Vcard; 689 } 690 691 /** 692 * Handler to emit vCards to PCE. 693 */ 694 public class HandlerForStringBuffer { 695 private Operation operation; 696 697 private OutputStream outputStream; 698 699 private String phoneOwnVCard = null; 700 701 public HandlerForStringBuffer(Operation op, String ownerVCard) { 702 operation = op; 703 if (ownerVCard != null) { 704 phoneOwnVCard = ownerVCard; 705 if (V) Log.v(TAG, "phone own number vcard:"); 706 if (V) Log.v(TAG, phoneOwnVCard); 707 } 708 } 709 710 private boolean write(String vCard) { 711 try { 712 if (vCard != null) { 713 outputStream.write(vCard.getBytes()); 714 return true; 715 } 716 } catch (IOException e) { 717 Log.e(TAG, "write outputstrem failed" + e.toString()); 718 } 719 return false; 720 } 721 722 public boolean onInit(Context context) { 723 try { 724 outputStream = operation.openOutputStream(); 725 if (phoneOwnVCard != null) { 726 return write(phoneOwnVCard); 727 } 728 return true; 729 } catch (IOException e) { 730 Log.e(TAG, "open outputstrem failed" + e.toString()); 731 } 732 return false; 733 } 734 735 public boolean onEntryCreated(String vcard) { 736 return write(vcard); 737 } 738 739 public void onTerminate() { 740 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 741 if (V) Log.v(TAG, "CloseStream failed!"); 742 } else { 743 if (V) Log.v(TAG, "CloseStream ok!"); 744 } 745 } 746 } 747 748 public static class VCardFilter { 749 private static enum FilterBit { 750 // bit property onlyCheckV21 excludeForV21 751 FN ( 1, "FN", true, false), 752 PHOTO( 3, "PHOTO", false, false), 753 BDAY( 4, "BDAY", false, false), 754 ADR( 5, "ADR", false, false), 755 EMAIL( 8, "EMAIL", false, false), 756 TITLE( 12, "TITLE", false, false), 757 ORG( 16, "ORG", false, false), 758 NOTE( 17, "NOTE", false, false), 759 URL( 20, "URL", false, false), 760 NICKNAME( 23, "NICKNAME", false, true), 761 DATETIME( 28, "DATETIME", false, true); 762 763 public final int pos; 764 public final String prop; 765 public final boolean onlyCheckV21; 766 public final boolean excludeForV21; 767 768 FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) { 769 this.pos = pos; 770 this.prop = prop; 771 this.onlyCheckV21 = onlyCheckV21; 772 this.excludeForV21 = excludeForV21; 773 } 774 } 775 776 private static final String SEPARATOR = System.getProperty("line.separator"); 777 private final byte[] filter; 778 779 //This function returns true if the attributes needs to be included in the filtered vcard. 780 private boolean isFilteredIn(FilterBit bit, boolean vCardType21) { 781 final int offset = (bit.pos / 8) + 1; 782 final int bit_pos = bit.pos % 8; 783 if (!vCardType21 && bit.onlyCheckV21) return true; 784 if (vCardType21 && bit.excludeForV21) return false; 785 if (filter == null || offset >= filter.length) return true; 786 return ((filter[filter.length - offset] >> bit_pos) & 0x01) != 0; 787 } 788 789 VCardFilter(byte[] filter) { 790 this.filter = filter; 791 } 792 793 public boolean isPhotoEnabled() { 794 return isFilteredIn(FilterBit.PHOTO, false); 795 } 796 797 public String apply(String vCard, boolean vCardType21){ 798 if (filter == null) return vCard; 799 String lines[] = vCard.split(SEPARATOR); 800 StringBuilder filteredVCard = new StringBuilder(); 801 boolean filteredIn = false; 802 803 for (String line : lines) { 804 // Check whether the current property is changing (ignoring multi-line properties) 805 // and determine if the current property is filtered in. 806 if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) { 807 String currentProp = line.split("[;:]")[0]; 808 filteredIn = true; 809 810 for (FilterBit bit : FilterBit.values()) { 811 if (bit.prop.equals(currentProp)) { 812 filteredIn = isFilteredIn(bit, vCardType21); 813 break; 814 } 815 } 816 817 // Since PBAP does not have filter bits for IM and SIP, 818 // exclude them by default. Easiest way is to exclude all 819 // X- fields.... 820 if (currentProp.startsWith("X-")) filteredIn = false; 821 } 822 823 // Build filtered vCard 824 if (filteredIn) filteredVCard.append(line + SEPARATOR); 825 } 826 827 return filteredVCard.toString(); 828 } 829 } 830 831 private static final Uri getPhoneLookupFilterUri() { 832 return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI; 833 } 834 835 /** 836 * Get size of the cursor without duplicated contact id. This assumes the 837 * given cursor is sorted by CONATCT_ID. 838 */ 839 private static final int getDistinctContactIdSize(Cursor cursor) { 840 final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); 841 final int idColumn = cursor.getColumnIndex(Data._ID); 842 long previousContactId = -1; 843 int count = 0; 844 cursor.moveToPosition(-1); 845 while (cursor.moveToNext()) { 846 final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn); 847 if (previousContactId != contactId) { 848 count++; 849 previousContactId = contactId; 850 } 851 } 852 if (V) { 853 Log.i(TAG, "getDistinctContactIdSize result: " + count); 854 } 855 return count; 856 } 857 858 /** 859 * Append "display_name,contact_id" string array from cursor to ArrayList. 860 * This assumes the given cursor is sorted by CONATCT_ID. 861 */ 862 private static void appendDistinctNameIdList(ArrayList<String> resultList, 863 String defaultName, Cursor cursor) { 864 final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); 865 final int idColumn = cursor.getColumnIndex(Data._ID); 866 final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME); 867 long previousContactId = -1; 868 cursor.moveToPosition(-1); 869 while (cursor.moveToNext()) { 870 final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn); 871 String displayName = nameColumn != -1 ? cursor.getString(nameColumn) : defaultName; 872 if (TextUtils.isEmpty(displayName)) { 873 displayName = defaultName; 874 } 875 876 if (previousContactId != contactId) { 877 previousContactId = contactId; 878 resultList.add(displayName + "," + contactId); 879 } 880 } 881 if (V) { 882 for (String nameId : resultList) { 883 Log.i(TAG, "appendDistinctNameIdList result: " + nameId); 884 } 885 } 886 } 887} 888