BluetoothPbapVcardManager.java revision d799e680c488c802ccad4b3eb23df525a5f86b44
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 // By default order is indexed 265 String orderBy = Phone.CONTACT_ID; 266 try { 267 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 268 orderBy = Phone.DISPLAY_NAME; 269 } 270 contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, 271 CLAUSE_ONLY_VISIBLE, null, orderBy); 272 if (contactCursor != null) { 273 appendDistinctNameIdList(nameList, 274 mContext.getString(android.R.string.unknownName), 275 contactCursor); 276 } 277 } catch (CursorWindowAllocationException e) { 278 Log.e(TAG, "CursorWindowAllocationException while getting phonebook name list"); 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 // By default order is indexed 449 String orderBy = Phone.CONTACT_ID; 450 try { 451 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 452 orderBy = Phone.DISPLAY_NAME; 453 } 454 contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, 455 CLAUSE_ONLY_VISIBLE, null, orderBy); 456 } catch (CursorWindowAllocationException e) { 457 Log.e(TAG, 458 "CursorWindowAllocationException while composing phonebook one vcard"); 459 } finally { 460 if (contactCursor != null) { 461 contactIdCursor = ContactCursorFilter.filterByOffset(contactCursor, offset); 462 contactCursor.close(); 463 contactCursor = null; 464 } 465 } 466 return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard, 467 ignorefilter, filter); 468 } 469 470 /** 471 * Filter contact cursor by certain condition. 472 */ 473 public static final class ContactCursorFilter { 474 /** 475 * 476 * @param contactCursor 477 * @param offset 478 * @return a cursor containing contact id of {@code offset} contact. 479 */ 480 public static Cursor filterByOffset(Cursor contactCursor, int offset) { 481 return filterByRange(contactCursor, offset, offset); 482 } 483 484 /** 485 * 486 * @param contactCursor 487 * @param startPoint 488 * @param endPoint 489 * @return a cursor containing contact ids of {@code startPoint}th to {@code endPoint}th 490 * contact. 491 */ 492 public static Cursor filterByRange(Cursor contactCursor, int startPoint, int endPoint) { 493 final int contactIdColumn = contactCursor.getColumnIndex(Data.CONTACT_ID); 494 long previousContactId = -1; 495 // As startPoint, endOffset index starts from 1 to n, we set 496 // currentPoint base as 1 not 0 497 int currentOffset = 1; 498 final MatrixCursor contactIdsCursor = new MatrixCursor(new String[]{ 499 Phone.CONTACT_ID 500 }); 501 while (contactCursor.moveToNext() && currentOffset <= endPoint) { 502 long currentContactId = contactCursor.getLong(contactIdColumn); 503 if (previousContactId != currentContactId) { 504 previousContactId = currentContactId; 505 if (currentOffset >= startPoint) { 506 contactIdsCursor.addRow(new Long[]{currentContactId}); 507 if (V) Log.v(TAG, "contactIdsCursor.addRow: " + currentContactId); 508 } 509 currentOffset++; 510 } 511 } 512 return contactIdsCursor; 513 } 514 } 515 516 /** 517 * Handler enterprise contact id in VCardComposer 518 */ 519 private static class EnterpriseRawContactEntitlesInfoCallback implements 520 VCardComposer.RawContactEntitlesInfoCallback { 521 @Override 522 public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) { 523 if (Contacts.isEnterpriseContactId(contactId)) { 524 return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI, 525 contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE); 526 } else { 527 return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI, contactId); 528 } 529 } 530 } 531 532 public final int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor, 533 final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) { 534 long timestamp = 0; 535 if (V) timestamp = System.currentTimeMillis(); 536 537 VCardComposer composer = null; 538 VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter); 539 540 HandlerForStringBuffer buffer = null; 541 try { 542 // Currently only support Generic Vcard 2.1 and 3.0 543 int vcardType; 544 if (vcardType21) { 545 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 546 } else { 547 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 548 } 549 if (!vcardfilter.isPhotoEnabled()) { 550 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 551 } 552 553 // Enhancement: customize Vcard based on preferences/settings and 554 // input from caller 555 composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null); 556 // End enhancement 557 558 // BT does want PAUSE/WAIT conversion while it doesn't want the 559 // other formatting 560 // done by vCard library by default. 561 composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() { 562 public String onValueReceived(String rawValue, int type, String label, 563 boolean isPrimary) { 564 // 'p' and 'w' are the standard characters for pause and 565 // wait 566 // (see RFC 3601) 567 // so use those when exporting phone numbers via vCard. 568 String numberWithControlSequence = rawValue 569 .replace(PhoneNumberUtils.PAUSE, 'p').replace(PhoneNumberUtils.WAIT, 570 'w'); 571 return numberWithControlSequence; 572 } 573 }); 574 buffer = new HandlerForStringBuffer(op, ownerVCard); 575 Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount()); 576 if (!composer.initWithCallback(contactIdCursor, 577 new EnterpriseRawContactEntitlesInfoCallback()) 578 || !buffer.onInit(mContext)) { 579 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 580 } 581 582 while (!composer.isAfterLast()) { 583 if (BluetoothPbapObexServer.sIsAborted) { 584 ((ServerOperation) op).isAborted = true; 585 BluetoothPbapObexServer.sIsAborted = false; 586 break; 587 } 588 String vcard = composer.createOneEntry(); 589 if (vcard == null) { 590 Log.e(TAG, 591 "Failed to read a contact. Error reason: " + composer.getErrorReason()); 592 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 593 } 594 if (V) Log.v(TAG, "vCard from composer: " + vcard); 595 596 vcard = vcardfilter.apply(vcard, vcardType21); 597 vcard = StripTelephoneNumber(vcard); 598 599 if (V) Log.v(TAG, "vCard after cleanup: " + vcard); 600 601 if (!buffer.onEntryCreated(vcard)) { 602 // onEntryCreate() already emits error. 603 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 604 } 605 } 606 } finally { 607 if (composer != null) { 608 composer.terminate(); 609 } 610 if (buffer != null) { 611 buffer.onTerminate(); 612 } 613 } 614 615 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 616 + (System.currentTimeMillis() - timestamp) + " ms"); 617 618 return ResponseCodes.OBEX_HTTP_OK; 619 } 620 621 public final int composeCallLogsAndSendVCards(Operation op, final String selection, 622 final boolean vcardType21, String ownerVCard, boolean ignorefilter, 623 byte[] filter) { 624 long timestamp = 0; 625 if (V) timestamp = System.currentTimeMillis(); 626 627 BluetoothPbapCallLogComposer composer = null; 628 HandlerForStringBuffer buffer = null; 629 try { 630 631 composer = new BluetoothPbapCallLogComposer(mContext); 632 buffer = new HandlerForStringBuffer(op, ownerVCard); 633 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER) 634 || !buffer.onInit(mContext)) { 635 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 636 } 637 638 while (!composer.isAfterLast()) { 639 if (BluetoothPbapObexServer.sIsAborted) { 640 ((ServerOperation) op).isAborted = true; 641 BluetoothPbapObexServer.sIsAborted = false; 642 break; 643 } 644 String vcard = composer.createOneEntry(vcardType21); 645 if (vcard == null) { 646 Log.e(TAG, 647 "Failed to read a contact. Error reason: " + composer.getErrorReason()); 648 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 649 } 650 if (V) { 651 Log.v(TAG, "Vcard Entry:"); 652 Log.v(TAG, vcard); 653 } 654 655 buffer.onEntryCreated(vcard); 656 } 657 } finally { 658 if (composer != null) { 659 composer.terminate(); 660 } 661 if (buffer != null) { 662 buffer.onTerminate(); 663 } 664 } 665 666 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 667 + (System.currentTimeMillis() - timestamp) + " ms"); 668 return ResponseCodes.OBEX_HTTP_OK; 669 } 670 671 public String StripTelephoneNumber (String vCard){ 672 String attr [] = vCard.split(System.getProperty("line.separator")); 673 String Vcard = ""; 674 for (int i=0; i < attr.length; i++) { 675 if(attr[i].startsWith("TEL")) { 676 attr[i] = attr[i].replace("(", ""); 677 attr[i] = attr[i].replace(")", ""); 678 attr[i] = attr[i].replace("-", ""); 679 attr[i] = attr[i].replace(" ", ""); 680 } 681 } 682 683 for (int i=0; i < attr.length; i++) { 684 if(!attr[i].equals("")){ 685 Vcard = Vcard.concat(attr[i] + "\n"); 686 } 687 } 688 if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard); 689 return Vcard; 690 } 691 692 /** 693 * Handler to emit vCards to PCE. 694 */ 695 public class HandlerForStringBuffer { 696 private Operation operation; 697 698 private OutputStream outputStream; 699 700 private String phoneOwnVCard = null; 701 702 public HandlerForStringBuffer(Operation op, String ownerVCard) { 703 operation = op; 704 if (ownerVCard != null) { 705 phoneOwnVCard = ownerVCard; 706 if (V) Log.v(TAG, "phone own number vcard:"); 707 if (V) Log.v(TAG, phoneOwnVCard); 708 } 709 } 710 711 private boolean write(String vCard) { 712 try { 713 if (vCard != null) { 714 outputStream.write(vCard.getBytes()); 715 return true; 716 } 717 } catch (IOException e) { 718 Log.e(TAG, "write outputstrem failed" + e.toString()); 719 } 720 return false; 721 } 722 723 public boolean onInit(Context context) { 724 try { 725 outputStream = operation.openOutputStream(); 726 if (phoneOwnVCard != null) { 727 return write(phoneOwnVCard); 728 } 729 return true; 730 } catch (IOException e) { 731 Log.e(TAG, "open outputstrem failed" + e.toString()); 732 } 733 return false; 734 } 735 736 public boolean onEntryCreated(String vcard) { 737 return write(vcard); 738 } 739 740 public void onTerminate() { 741 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 742 if (V) Log.v(TAG, "CloseStream failed!"); 743 } else { 744 if (V) Log.v(TAG, "CloseStream ok!"); 745 } 746 } 747 } 748 749 public static class VCardFilter { 750 private static enum FilterBit { 751 // bit property onlyCheckV21 excludeForV21 752 FN ( 1, "FN", true, false), 753 PHOTO( 3, "PHOTO", false, false), 754 BDAY( 4, "BDAY", false, false), 755 ADR( 5, "ADR", false, false), 756 EMAIL( 8, "EMAIL", false, false), 757 TITLE( 12, "TITLE", false, false), 758 ORG( 16, "ORG", false, false), 759 NOTE( 17, "NOTE", false, false), 760 URL( 20, "URL", false, false), 761 NICKNAME( 23, "NICKNAME", false, true), 762 DATETIME( 28, "DATETIME", false, true); 763 764 public final int pos; 765 public final String prop; 766 public final boolean onlyCheckV21; 767 public final boolean excludeForV21; 768 769 FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) { 770 this.pos = pos; 771 this.prop = prop; 772 this.onlyCheckV21 = onlyCheckV21; 773 this.excludeForV21 = excludeForV21; 774 } 775 } 776 777 private static final String SEPARATOR = System.getProperty("line.separator"); 778 private final byte[] filter; 779 780 //This function returns true if the attributes needs to be included in the filtered vcard. 781 private boolean isFilteredIn(FilterBit bit, boolean vCardType21) { 782 final int offset = (bit.pos / 8) + 1; 783 final int bit_pos = bit.pos % 8; 784 if (!vCardType21 && bit.onlyCheckV21) return true; 785 if (vCardType21 && bit.excludeForV21) return false; 786 if (filter == null || offset >= filter.length) return true; 787 return ((filter[filter.length - offset] >> bit_pos) & 0x01) != 0; 788 } 789 790 VCardFilter(byte[] filter) { 791 this.filter = filter; 792 } 793 794 public boolean isPhotoEnabled() { 795 return isFilteredIn(FilterBit.PHOTO, false); 796 } 797 798 public String apply(String vCard, boolean vCardType21){ 799 if (filter == null) return vCard; 800 String lines[] = vCard.split(SEPARATOR); 801 StringBuilder filteredVCard = new StringBuilder(); 802 boolean filteredIn = false; 803 804 for (String line : lines) { 805 // Check whether the current property is changing (ignoring multi-line properties) 806 // and determine if the current property is filtered in. 807 if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) { 808 String currentProp = line.split("[;:]")[0]; 809 filteredIn = true; 810 811 for (FilterBit bit : FilterBit.values()) { 812 if (bit.prop.equals(currentProp)) { 813 filteredIn = isFilteredIn(bit, vCardType21); 814 break; 815 } 816 } 817 818 // Since PBAP does not have filter bits for IM and SIP, 819 // exclude them by default. Easiest way is to exclude all 820 // X- fields.... 821 if (currentProp.startsWith("X-")) filteredIn = false; 822 } 823 824 // Build filtered vCard 825 if (filteredIn) filteredVCard.append(line + SEPARATOR); 826 } 827 828 return filteredVCard.toString(); 829 } 830 } 831 832 private static final Uri getPhoneLookupFilterUri() { 833 return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI; 834 } 835 836 /** 837 * Get size of the cursor without duplicated contact id. This assumes the 838 * given cursor is sorted by CONTACT_ID. 839 */ 840 private static final int getDistinctContactIdSize(Cursor cursor) { 841 final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); 842 final int idColumn = cursor.getColumnIndex(Data._ID); 843 long previousContactId = -1; 844 int count = 0; 845 cursor.moveToPosition(-1); 846 while (cursor.moveToNext()) { 847 final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn); 848 if (previousContactId != contactId) { 849 count++; 850 previousContactId = contactId; 851 } 852 } 853 if (V) { 854 Log.i(TAG, "getDistinctContactIdSize result: " + count); 855 } 856 return count; 857 } 858 859 /** 860 * Append "display_name,contact_id" string array from cursor to ArrayList. 861 * This assumes the given cursor is sorted by CONTACT_ID. 862 */ 863 private static void appendDistinctNameIdList(ArrayList<String> resultList, 864 String defaultName, Cursor cursor) { 865 final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); 866 final int idColumn = cursor.getColumnIndex(Data._ID); 867 final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME); 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 String newString = displayName + "," + contactId; 877 if (!resultList.contains(newString)) { 878 resultList.add(newString); 879 } 880 } 881 if (V) { 882 for (String nameId : resultList) { 883 Log.i(TAG, "appendDistinctNameIdList result: " + nameId); 884 } 885 } 886 } 887} 888