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