BluetoothPbapVcardManager.java revision b211df6fcbd7dcd055354b75afc2e68ea49a36d9
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 composer = new BluetoothPbapCallLogComposer(mContext); 627 buffer = new HandlerForStringBuffer(op, ownerVCard); 628 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER) 629 || !buffer.onInit(mContext)) { 630 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 631 } 632 633 while (!composer.isAfterLast()) { 634 if (BluetoothPbapObexServer.sIsAborted) { 635 ((ServerOperation) op).isAborted = true; 636 BluetoothPbapObexServer.sIsAborted = false; 637 break; 638 } 639 String vcard = composer.createOneEntry(vcardType21); 640 if (vcard == null) { 641 Log.e(TAG, 642 "Failed to read a contact. Error reason: " + composer.getErrorReason()); 643 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 644 } 645 if (V) { 646 Log.v(TAG, "Vcard Entry:"); 647 Log.v(TAG, vcard); 648 } 649 650 buffer.onEntryCreated(vcard); 651 } 652 } finally { 653 if (composer != null) { 654 composer.terminate(); 655 } 656 if (buffer != null) { 657 buffer.onTerminate(); 658 } 659 } 660 661 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 662 + (System.currentTimeMillis() - timestamp) + " ms"); 663 return ResponseCodes.OBEX_HTTP_OK; 664 } 665 666 public String StripTelephoneNumber (String vCard){ 667 String attr [] = vCard.split(System.getProperty("line.separator")); 668 String Vcard = ""; 669 for (int i=0; i < attr.length; i++) { 670 if(attr[i].startsWith("TEL")) { 671 attr[i] = attr[i].replace("(", ""); 672 attr[i] = attr[i].replace(")", ""); 673 attr[i] = attr[i].replace("-", ""); 674 attr[i] = attr[i].replace(" ", ""); 675 } 676 } 677 678 for (int i=0; i < attr.length; i++) { 679 if(!attr[i].equals("")){ 680 Vcard = Vcard.concat(attr[i] + "\n"); 681 } 682 } 683 if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard); 684 return Vcard; 685 } 686 687 /** 688 * Handler to emit vCards to PCE. 689 */ 690 public class HandlerForStringBuffer { 691 private Operation operation; 692 693 private OutputStream outputStream; 694 695 private String phoneOwnVCard = null; 696 697 public HandlerForStringBuffer(Operation op, String ownerVCard) { 698 operation = op; 699 if (ownerVCard != null) { 700 phoneOwnVCard = ownerVCard; 701 if (V) Log.v(TAG, "phone own number vcard:"); 702 if (V) Log.v(TAG, phoneOwnVCard); 703 } 704 } 705 706 private boolean write(String vCard) { 707 try { 708 if (vCard != null) { 709 outputStream.write(vCard.getBytes()); 710 return true; 711 } 712 } catch (IOException e) { 713 Log.e(TAG, "write outputstrem failed" + e.toString()); 714 } 715 return false; 716 } 717 718 public boolean onInit(Context context) { 719 try { 720 outputStream = operation.openOutputStream(); 721 if (phoneOwnVCard != null) { 722 return write(phoneOwnVCard); 723 } 724 return true; 725 } catch (IOException e) { 726 Log.e(TAG, "open outputstrem failed" + e.toString()); 727 } 728 return false; 729 } 730 731 public boolean onEntryCreated(String vcard) { 732 return write(vcard); 733 } 734 735 public void onTerminate() { 736 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 737 if (V) Log.v(TAG, "CloseStream failed!"); 738 } else { 739 if (V) Log.v(TAG, "CloseStream ok!"); 740 } 741 } 742 } 743 744 public static class VCardFilter { 745 private static enum FilterBit { 746 // bit property onlyCheckV21 excludeForV21 747 FN ( 1, "FN", true, false), 748 PHOTO( 3, "PHOTO", false, false), 749 BDAY( 4, "BDAY", false, false), 750 ADR( 5, "ADR", false, false), 751 EMAIL( 8, "EMAIL", false, false), 752 TITLE( 12, "TITLE", false, false), 753 ORG( 16, "ORG", false, false), 754 NOTE( 17, "NOTE", false, false), 755 URL( 20, "URL", false, false), 756 NICKNAME( 23, "NICKNAME", false, true), 757 DATETIME( 28, "DATETIME", false, true); 758 759 public final int pos; 760 public final String prop; 761 public final boolean onlyCheckV21; 762 public final boolean excludeForV21; 763 764 FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) { 765 this.pos = pos; 766 this.prop = prop; 767 this.onlyCheckV21 = onlyCheckV21; 768 this.excludeForV21 = excludeForV21; 769 } 770 } 771 772 private static final String SEPARATOR = System.getProperty("line.separator"); 773 private final byte[] filter; 774 775 //This function returns true if the attributes needs to be included in the filtered vcard. 776 private boolean isFilteredIn(FilterBit bit, boolean vCardType21) { 777 final int offset = (bit.pos / 8) + 1; 778 final int bit_pos = bit.pos % 8; 779 if (!vCardType21 && bit.onlyCheckV21) return true; 780 if (vCardType21 && bit.excludeForV21) return false; 781 if (filter == null || offset >= filter.length) return true; 782 return ((filter[filter.length - offset] >> bit_pos) & 0x01) != 0; 783 } 784 785 VCardFilter(byte[] filter) { 786 this.filter = filter; 787 } 788 789 public boolean isPhotoEnabled() { 790 return isFilteredIn(FilterBit.PHOTO, false); 791 } 792 793 public String apply(String vCard, boolean vCardType21){ 794 if (filter == null) return vCard; 795 String lines[] = vCard.split(SEPARATOR); 796 StringBuilder filteredVCard = new StringBuilder(); 797 boolean filteredIn = false; 798 799 for (String line : lines) { 800 // Check whether the current property is changing (ignoring multi-line properties) 801 // and determine if the current property is filtered in. 802 if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) { 803 String currentProp = line.split("[;:]")[0]; 804 filteredIn = true; 805 806 for (FilterBit bit : FilterBit.values()) { 807 if (bit.prop.equals(currentProp)) { 808 filteredIn = isFilteredIn(bit, vCardType21); 809 break; 810 } 811 } 812 813 // Since PBAP does not have filter bits for IM and SIP, 814 // exclude them by default. Easiest way is to exclude all 815 // X- fields.... 816 if (currentProp.startsWith("X-")) filteredIn = false; 817 } 818 819 // Build filtered vCard 820 if (filteredIn) filteredVCard.append(line + SEPARATOR); 821 } 822 823 return filteredVCard.toString(); 824 } 825 } 826 827 private static final Uri getPhoneLookupFilterUri() { 828 return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI; 829 } 830 831 /** 832 * Get size of the cursor without duplicated contact id. This assumes the 833 * given cursor is sorted by CONATCT_ID. 834 */ 835 private static final int getDistinctContactIdSize(Cursor cursor) { 836 final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); 837 final int idColumn = cursor.getColumnIndex(Data._ID); 838 long previousContactId = -1; 839 int count = 0; 840 cursor.moveToPosition(-1); 841 while (cursor.moveToNext()) { 842 final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn); 843 if (previousContactId != contactId) { 844 count++; 845 previousContactId = contactId; 846 } 847 } 848 if (V) { 849 Log.i(TAG, "getDistinctContactIdSize result: " + count); 850 } 851 return count; 852 } 853 854 /** 855 * Append "display_name,contact_id" string array from cursor to ArrayList. 856 * This assumes the given cursor is sorted by CONATCT_ID. 857 */ 858 private static void appendDistinctNameIdList(ArrayList<String> resultList, 859 String defaultName, Cursor cursor) { 860 final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); 861 final int idColumn = cursor.getColumnIndex(Data._ID); 862 final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME); 863 long previousContactId = -1; 864 cursor.moveToPosition(-1); 865 while (cursor.moveToNext()) { 866 final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn); 867 String displayName = nameColumn != -1 ? cursor.getString(nameColumn) : defaultName; 868 if (TextUtils.isEmpty(displayName)) { 869 displayName = defaultName; 870 } 871 872 if (previousContactId != contactId) { 873 previousContactId = contactId; 874 resultList.add(displayName + "," + contactId); 875 } 876 } 877 if (V) { 878 for (String nameId : resultList) { 879 Log.i(TAG, "appendDistinctNameIdList result: " + nameId); 880 } 881 } 882 } 883} 884