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