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; 59import java.nio.ByteBuffer; 60import java.util.Collections; 61import java.util.Comparator; 62import com.android.bluetooth.R; 63import com.android.vcard.VCardComposer; 64import com.android.vcard.VCardConfig; 65import com.android.vcard.VCardPhoneNumberTranslationCallback; 66 67import java.io.IOException; 68import java.io.OutputStream; 69import java.util.ArrayList; 70 71import javax.obex.Operation; 72import javax.obex.ResponseCodes; 73import javax.obex.ServerOperation; 74 75public class BluetoothPbapVcardManager { 76 private static final String TAG = "BluetoothPbapVcardManager"; 77 78 private static final boolean V = BluetoothPbapService.VERBOSE; 79 80 private ContentResolver mResolver; 81 82 private Context mContext; 83 84 private static final int PHONE_NUMBER_COLUMN_INDEX = 3; 85 86 static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC"; 87 88 static final String[] PHONES_CONTACTS_PROJECTION = new String[] { 89 Phone.CONTACT_ID, // 0 90 Phone.DISPLAY_NAME, // 1 91 }; 92 93 static final String[] PHONE_LOOKUP_PROJECTION = new String[] { 94 PhoneLookup._ID, PhoneLookup.DISPLAY_NAME 95 }; 96 97 static final int CONTACTS_ID_COLUMN_INDEX = 0; 98 99 static final int CONTACTS_NAME_COLUMN_INDEX = 1; 100 101 static long LAST_FETCHED_TIME_STAMP; 102 103 // call histories use dynamic handles, and handles should order by date; the 104 // most recently one should be the first handle. In table "calls", _id and 105 // date are consistent in ordering, to implement simply, we sort by _id 106 // here. 107 static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC"; 108 109 private static final int NEED_SEND_BODY = -1; 110 111 public BluetoothPbapVcardManager(final Context context) { 112 mContext = context; 113 mResolver = mContext.getContentResolver(); 114 LAST_FETCHED_TIME_STAMP = System.currentTimeMillis(); 115 } 116 117 /** 118 * Create an owner vcard from the configured profile 119 * @param vcardType21 120 * @return 121 */ 122 private final String getOwnerPhoneNumberVcardFromProfile( 123 final boolean vcardType21, final byte[] filter) { 124 // Currently only support Generic Vcard 2.1 and 3.0 125 int vcardType; 126 if (vcardType21) { 127 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 128 } else { 129 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 130 } 131 132 if (!BluetoothPbapConfig.includePhotosInVcard()) { 133 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 134 } 135 136 return BluetoothPbapUtils.createProfileVCard(mContext, vcardType,filter); 137 } 138 139 public final String getOwnerPhoneNumberVcard(final boolean vcardType21, final byte[] filter) { 140 //Owner vCard enhancement: Use "ME" profile if configured 141 if (BluetoothPbapConfig.useProfileForOwnerVcard()) { 142 String vcard = getOwnerPhoneNumberVcardFromProfile(vcardType21, filter); 143 if (vcard != null && vcard.length() != 0) { 144 return vcard; 145 } 146 } 147 //End enhancement 148 149 BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext); 150 String name = BluetoothPbapService.getLocalPhoneName(); 151 String number = BluetoothPbapService.getLocalPhoneNum(); 152 String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number, 153 vcardType21); 154 return vcard; 155 } 156 157 public final int getPhonebookSize(final int type) { 158 int size; 159 switch (type) { 160 case BluetoothPbapObexServer.ContentType.PHONEBOOK: 161 size = getContactsSize(); 162 break; 163 default: 164 size = getCallHistorySize(type); 165 break; 166 } 167 if (V) Log.v(TAG, "getPhonebookSize size = " + size + " type = " + type); 168 return size; 169 } 170 171 public final int getContactsSize() { 172 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 173 Cursor contactCursor = null; 174 try { 175 contactCursor = mResolver.query( 176 myUri, new String[] {Phone.CONTACT_ID}, null, null, Phone.CONTACT_ID); 177 if (contactCursor == null) { 178 return 0; 179 } 180 return getDistinctContactIdSize(contactCursor) + 1; // always has the 0.vcf 181 } catch (CursorWindowAllocationException e) { 182 Log.e(TAG, "CursorWindowAllocationException while getting Contacts size"); 183 } finally { 184 if (contactCursor != null) { 185 contactCursor.close(); 186 } 187 } 188 return 0; 189 } 190 191 public final int getCallHistorySize(final int type) { 192 final Uri myUri = CallLog.Calls.CONTENT_URI; 193 String selection = BluetoothPbapObexServer.createSelectionPara(type); 194 int size = 0; 195 Cursor callCursor = null; 196 try { 197 callCursor = mResolver.query(myUri, null, selection, null, 198 CallLog.Calls.DEFAULT_SORT_ORDER); 199 if (callCursor != null) { 200 size = callCursor.getCount(); 201 } 202 } catch (CursorWindowAllocationException e) { 203 Log.e(TAG, "CursorWindowAllocationException while getting CallHistory size"); 204 } finally { 205 if (callCursor != null) { 206 callCursor.close(); 207 callCursor = null; 208 } 209 } 210 return size; 211 } 212 213 public final ArrayList<String> loadCallHistoryList(final int type) { 214 final Uri myUri = CallLog.Calls.CONTENT_URI; 215 String selection = BluetoothPbapObexServer.createSelectionPara(type); 216 String[] projection = new String[] { 217 Calls.NUMBER, Calls.CACHED_NAME, Calls.NUMBER_PRESENTATION 218 }; 219 final int CALLS_NUMBER_COLUMN_INDEX = 0; 220 final int CALLS_NAME_COLUMN_INDEX = 1; 221 final int CALLS_NUMBER_PRESENTATION_COLUMN_INDEX = 2; 222 223 Cursor callCursor = null; 224 ArrayList<String> list = new ArrayList<String>(); 225 try { 226 callCursor = mResolver.query(myUri, projection, selection, null, 227 CALLLOG_SORT_ORDER); 228 if (callCursor != null) { 229 for (callCursor.moveToFirst(); !callCursor.isAfterLast(); 230 callCursor.moveToNext()) { 231 String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX); 232 if (TextUtils.isEmpty(name)) { 233 // name not found, use number instead 234 final int numberPresentation = callCursor.getInt( 235 CALLS_NUMBER_PRESENTATION_COLUMN_INDEX); 236 if (numberPresentation != Calls.PRESENTATION_ALLOWED) { 237 name = mContext.getString(R.string.unknownNumber); 238 } else { 239 name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX); 240 } 241 } 242 list.add(name); 243 } 244 } 245 } catch (CursorWindowAllocationException e) { 246 Log.e(TAG, "CursorWindowAllocationException while loading CallHistory"); 247 } finally { 248 if (callCursor != null) { 249 callCursor.close(); 250 callCursor = null; 251 } 252 } 253 return list; 254 } 255 256 public final ArrayList<String> getPhonebookNameList(final int orderByWhat) { 257 ArrayList<String> nameList = new ArrayList<String>(); 258 //Owner vCard enhancement. Use "ME" profile if configured 259 String ownerName = null; 260 if (BluetoothPbapConfig.useProfileForOwnerVcard()) { 261 ownerName = BluetoothPbapUtils.getProfileName(mContext); 262 } 263 if (ownerName == null || ownerName.length()==0) { 264 ownerName = BluetoothPbapService.getLocalPhoneName(); 265 } 266 nameList.add(ownerName); 267 //End enhancement 268 269 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 270 Cursor contactCursor = null; 271 // By default order is indexed 272 String orderBy = Phone.CONTACT_ID; 273 try { 274 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 275 orderBy = Phone.DISPLAY_NAME; 276 } 277 contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy); 278 if (contactCursor != null) { 279 appendDistinctNameIdList(nameList, 280 mContext.getString(android.R.string.unknownName), 281 contactCursor); 282 } 283 } catch (CursorWindowAllocationException e) { 284 Log.e(TAG, "CursorWindowAllocationException while getting phonebook name list"); 285 } catch (Exception e) { 286 Log.e(TAG, "Exception while getting phonebook name list", e); 287 } finally { 288 if (contactCursor != null) { 289 contactCursor.close(); 290 contactCursor = null; 291 } 292 } 293 return nameList; 294 } 295 296 final ArrayList<String> getSelectedPhonebookNameList(final int orderByWhat, 297 final boolean vcardType21, int needSendBody, int pbSize, byte[] selector, 298 String vcardselectorop) { 299 ArrayList<String> nameList = new ArrayList<String>(); 300 PropertySelector vcardselector = new PropertySelector(selector); 301 VCardComposer composer = null; 302 int vcardType; 303 304 if (vcardType21) { 305 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 306 } else { 307 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 308 } 309 310 composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null); 311 composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() { 312 313 public String onValueReceived( 314 String rawValue, int type, String label, boolean isPrimary) { 315 String numberWithControlSequence = rawValue.replace(PhoneNumberUtils.PAUSE, 'p') 316 .replace(PhoneNumberUtils.WAIT, 'w'); 317 return numberWithControlSequence; 318 } 319 }); 320 321 // Owner vCard enhancement. Use "ME" profile if configured 322 String ownerName = null; 323 if (BluetoothPbapConfig.useProfileForOwnerVcard()) { 324 ownerName = BluetoothPbapUtils.getProfileName(mContext); 325 } 326 if (ownerName == null || ownerName.length() == 0) { 327 ownerName = BluetoothPbapService.getLocalPhoneName(); 328 } 329 nameList.add(ownerName); 330 // End enhancement 331 332 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 333 Cursor contactCursor = null; 334 try { 335 contactCursor = mResolver.query( 336 myUri, PHONES_CONTACTS_PROJECTION, null, null, Phone.CONTACT_ID); 337 338 if (contactCursor != null) { 339 if (!composer.initWithCallback( 340 contactCursor, new EnterpriseRawContactEntitlesInfoCallback())) { 341 return nameList; 342 } 343 344 while (!composer.isAfterLast()) { 345 String vcard = composer.createOneEntry(); 346 if (vcard == null) { 347 Log.e(TAG, "Failed to read a contact. Error reason: " 348 + composer.getErrorReason()); 349 return nameList; 350 } 351 if (V) Log.v(TAG, "Checking selected bits in the vcard composer" + vcard); 352 353 if (!vcardselector.CheckVcardSelector(vcard, vcardselectorop)) { 354 Log.e(TAG, "vcard selector check fail"); 355 vcard = null; 356 pbSize--; 357 continue; 358 } else { 359 String name = vcardselector.getName(vcard); 360 if (TextUtils.isEmpty(name)) { 361 name = mContext.getString(android.R.string.unknownName); 362 } 363 nameList.add(name); 364 } 365 } 366 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 367 if (V) Log.v(TAG, "getPhonebookNameList, order by index"); 368 // Do not need to do anything, as we sort it by index already 369 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 370 if (V) Log.v(TAG, "getPhonebookNameList, order by alpha"); 371 Collections.sort(nameList); 372 } 373 } 374 } catch (CursorWindowAllocationException e) { 375 Log.e(TAG, "CursorWindowAllocationException while getting Phonebook name list"); 376 } finally { 377 if (contactCursor != null) { 378 contactCursor.close(); 379 contactCursor = null; 380 } 381 } 382 return nameList; 383 } 384 385 public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) { 386 ArrayList<String> nameList = new ArrayList<String>(); 387 ArrayList<String> tempNameList = new ArrayList<String>(); 388 389 Cursor contactCursor = null; 390 Uri uri = null; 391 String[] projection = null; 392 393 if (TextUtils.isEmpty(phoneNumber)) { 394 uri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 395 projection = PHONES_CONTACTS_PROJECTION; 396 } else { 397 uri = Uri.withAppendedPath(getPhoneLookupFilterUri(), 398 Uri.encode(phoneNumber)); 399 projection = PHONE_LOOKUP_PROJECTION; 400 } 401 402 try { 403 contactCursor = mResolver.query(uri, projection, null, null, Phone.CONTACT_ID); 404 405 if (contactCursor != null) { 406 appendDistinctNameIdList(nameList, 407 mContext.getString(android.R.string.unknownName), 408 contactCursor); 409 if (V) { 410 for (String nameIdStr : nameList) { 411 Log.v(TAG, "got name " + nameIdStr + " by number " + phoneNumber); 412 } 413 } 414 } 415 } catch (CursorWindowAllocationException e) { 416 Log.e(TAG, "CursorWindowAllocationException while getting contact names"); 417 } finally { 418 if (contactCursor != null) { 419 contactCursor.close(); 420 contactCursor = null; 421 } 422 } 423 int tempListSize = tempNameList.size(); 424 for (int index = 0; index < tempListSize; index++) { 425 String object = tempNameList.get(index); 426 if (!nameList.contains(object)) 427 nameList.add(object); 428 } 429 430 return nameList; 431 } 432 433 byte[] getCallHistoryPrimaryFolderVersion(final int type) { 434 final Uri myUri = CallLog.Calls.CONTENT_URI; 435 String selection = BluetoothPbapObexServer.createSelectionPara(type); 436 selection = selection + " AND date >= " + LAST_FETCHED_TIME_STAMP; 437 438 Log.d(TAG, "LAST_FETCHED_TIME_STAMP is " + LAST_FETCHED_TIME_STAMP); 439 Cursor callCursor = null; 440 long count = 0; 441 long primaryVcMsb = 0; 442 ArrayList<String> list = new ArrayList<String>(); 443 try { 444 callCursor = mResolver.query(myUri, null, selection, null, null); 445 while (callCursor != null && callCursor.moveToNext()) { 446 count = count + 1; 447 } 448 } catch (Exception e) { 449 Log.e(TAG, "exception while fetching callHistory pvc"); 450 } finally { 451 if (callCursor != null) { 452 callCursor.close(); 453 callCursor = null; 454 } 455 } 456 457 LAST_FETCHED_TIME_STAMP = System.currentTimeMillis(); 458 Log.d(TAG, "getCallHistoryPrimaryFolderVersion count is " + count + " type is " + type); 459 ByteBuffer pvc = ByteBuffer.allocate(16); 460 pvc.putLong(primaryVcMsb); 461 Log.d(TAG, "primaryVersionCounter is " + BluetoothPbapUtils.primaryVersionCounter); 462 pvc.putLong(count); 463 return pvc.array(); 464 } 465 466 final int composeAndSendSelectedCallLogVcards(final int type, Operation op, 467 final int startPoint, final int endPoint, final boolean vcardType21, int needSendBody, 468 int pbSize, boolean ignorefilter, byte[] filter, byte[] vcardselector, 469 String vcardselectorop, boolean vcardselect) { 470 if (startPoint < 1 || startPoint > endPoint) { 471 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 472 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 473 } 474 String typeSelection = BluetoothPbapObexServer.createSelectionPara(type); 475 476 final Uri myUri = CallLog.Calls.CONTENT_URI; 477 final String[] CALLLOG_PROJECTION = new String[] { 478 CallLog.Calls._ID, // 0 479 }; 480 final int ID_COLUMN_INDEX = 0; 481 482 Cursor callsCursor = null; 483 long startPointId = 0; 484 long endPointId = 0; 485 try { 486 // Need test to see if order by _ID is ok here, or by date? 487 callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null, 488 CALLLOG_SORT_ORDER); 489 if (callsCursor != null) { 490 callsCursor.moveToPosition(startPoint - 1); 491 startPointId = callsCursor.getLong(ID_COLUMN_INDEX); 492 if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId); 493 if (startPoint == endPoint) { 494 endPointId = startPointId; 495 } else { 496 callsCursor.moveToPosition(endPoint - 1); 497 endPointId = callsCursor.getLong(ID_COLUMN_INDEX); 498 } 499 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId); 500 } 501 } catch (CursorWindowAllocationException e) { 502 Log.e(TAG, "CursorWindowAllocationException while composing calllog vcards"); 503 } finally { 504 if (callsCursor != null) { 505 callsCursor.close(); 506 callsCursor = null; 507 } 508 } 509 510 String recordSelection; 511 if (startPoint == endPoint) { 512 recordSelection = Calls._ID + "=" + startPointId; 513 } else { 514 // The query to call table is by "_id DESC" order, so change 515 // correspondingly. 516 recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<=" 517 + startPointId; 518 } 519 520 String selection; 521 if (typeSelection == null) { 522 selection = recordSelection; 523 } else { 524 selection = "(" + typeSelection + ") AND (" + recordSelection + ")"; 525 } 526 527 if (V) Log.v(TAG, "Call log query selection is: " + selection); 528 529 return composeCallLogsAndSendSelectedVCards(op, selection, vcardType21, needSendBody, 530 pbSize, null, ignorefilter, filter, vcardselector, vcardselectorop, vcardselect); 531 } 532 533 final int composeAndSendPhonebookVcards(Operation op, final int startPoint, final int endPoint, 534 final boolean vcardType21, String ownerVCard, int needSendBody, int pbSize, 535 boolean ignorefilter, byte[] filter, byte[] vcardselector, String vcardselectorop, 536 boolean vcardselect) { 537 if (startPoint < 1 || startPoint > endPoint) { 538 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 539 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 540 } 541 542 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 543 Cursor contactCursor = null; 544 Cursor contactIdCursor = new MatrixCursor(new String[] { 545 Phone.CONTACT_ID 546 }); 547 try { 548 contactCursor = mResolver.query( 549 myUri, PHONES_CONTACTS_PROJECTION, null, null, Phone.CONTACT_ID); 550 if (contactCursor != null) { 551 contactIdCursor = ContactCursorFilter.filterByRange(contactCursor, startPoint, 552 endPoint); 553 } 554 } catch (CursorWindowAllocationException e) { 555 Log.e(TAG, "CursorWindowAllocationException while composing phonebook vcards"); 556 } finally { 557 if (contactCursor != null) { 558 contactCursor.close(); 559 } 560 } 561 562 if (vcardselect) 563 return composeContactsAndSendSelectedVCards(op, contactIdCursor, vcardType21, 564 ownerVCard, needSendBody, pbSize, ignorefilter, filter, vcardselector, 565 vcardselectorop); 566 else 567 return composeContactsAndSendVCards( 568 op, contactIdCursor, vcardType21, ownerVCard, ignorefilter, filter); 569 } 570 571 final int composeAndSendPhonebookOneVcard(Operation op, final int offset, 572 final boolean vcardType21, String ownerVCard, int orderByWhat, boolean ignorefilter, 573 byte[] filter) { 574 if (offset < 1) { 575 Log.e(TAG, "Internal error: offset is not correct."); 576 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 577 } 578 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); 579 580 Cursor contactCursor = null; 581 Cursor contactIdCursor = new MatrixCursor(new String[] { 582 Phone.CONTACT_ID 583 }); 584 // By default order is indexed 585 String orderBy = Phone.CONTACT_ID; 586 try { 587 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 588 orderBy = Phone.DISPLAY_NAME; 589 } 590 contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy); 591 } catch (CursorWindowAllocationException e) { 592 Log.e(TAG, 593 "CursorWindowAllocationException while composing phonebook one vcard"); 594 } finally { 595 if (contactCursor != null) { 596 contactIdCursor = ContactCursorFilter.filterByOffset(contactCursor, offset); 597 contactCursor.close(); 598 contactCursor = null; 599 } 600 } 601 return composeContactsAndSendVCards( 602 op, contactIdCursor, vcardType21, ownerVCard, ignorefilter, filter); 603 } 604 605 /** 606 * Filter contact cursor by certain condition. 607 */ 608 private static final class ContactCursorFilter { 609 /** 610 * 611 * @param contactCursor 612 * @param offset 613 * @return a cursor containing contact id of {@code offset} contact. 614 */ 615 public static Cursor filterByOffset(Cursor contactCursor, int offset) { 616 return filterByRange(contactCursor, offset, offset); 617 } 618 619 /** 620 * 621 * @param contactCursor 622 * @param startPoint 623 * @param endPoint 624 * @return a cursor containing contact ids of {@code startPoint}th to {@code endPoint}th 625 * contact. 626 */ 627 public static Cursor filterByRange(Cursor contactCursor, int startPoint, int endPoint) { 628 final int contactIdColumn = contactCursor.getColumnIndex(Data.CONTACT_ID); 629 long previousContactId = -1; 630 // As startPoint, endOffset index starts from 1 to n, we set 631 // currentPoint base as 1 not 0 632 int currentOffset = 1; 633 final MatrixCursor contactIdsCursor = new MatrixCursor(new String[]{ 634 Phone.CONTACT_ID 635 }); 636 while (contactCursor.moveToNext() && currentOffset <= endPoint) { 637 long currentContactId = contactCursor.getLong(contactIdColumn); 638 if (previousContactId != currentContactId) { 639 previousContactId = currentContactId; 640 if (currentOffset >= startPoint) { 641 contactIdsCursor.addRow(new Long[]{currentContactId}); 642 if (V) Log.v(TAG, "contactIdsCursor.addRow: " + currentContactId); 643 } 644 currentOffset++; 645 } 646 } 647 return contactIdsCursor; 648 } 649 } 650 651 /** 652 * Handler enterprise contact id in VCardComposer 653 */ 654 private static class EnterpriseRawContactEntitlesInfoCallback implements 655 VCardComposer.RawContactEntitlesInfoCallback { 656 @Override 657 public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) { 658 if (Contacts.isEnterpriseContactId(contactId)) { 659 return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI, 660 contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE); 661 } else { 662 return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI, contactId); 663 } 664 } 665 } 666 667 private final int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor, 668 final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) { 669 long timestamp = 0; 670 if (V) timestamp = System.currentTimeMillis(); 671 672 VCardComposer composer = null; 673 VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter); 674 675 HandlerForStringBuffer buffer = null; 676 try { 677 // Currently only support Generic Vcard 2.1 and 3.0 678 int vcardType; 679 if (vcardType21) { 680 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 681 } else { 682 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 683 } 684 if (!vcardfilter.isPhotoEnabled()) { 685 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 686 } 687 688 // Enhancement: customize Vcard based on preferences/settings and 689 // input from caller 690 composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null); 691 // End enhancement 692 693 // BT does want PAUSE/WAIT conversion while it doesn't want the 694 // other formatting 695 // done by vCard library by default. 696 composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() { 697 public String onValueReceived(String rawValue, int type, String label, 698 boolean isPrimary) { 699 // 'p' and 'w' are the standard characters for pause and 700 // wait 701 // (see RFC 3601) 702 // so use those when exporting phone numbers via vCard. 703 String numberWithControlSequence = rawValue 704 .replace(PhoneNumberUtils.PAUSE, 'p').replace(PhoneNumberUtils.WAIT, 705 'w'); 706 return numberWithControlSequence; 707 } 708 }); 709 buffer = new HandlerForStringBuffer(op, ownerVCard); 710 Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount()); 711 if (!composer.initWithCallback(contactIdCursor, 712 new EnterpriseRawContactEntitlesInfoCallback()) 713 || !buffer.onInit(mContext)) { 714 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 715 } 716 717 while (!composer.isAfterLast()) { 718 if (BluetoothPbapObexServer.sIsAborted) { 719 ((ServerOperation) op).isAborted = true; 720 BluetoothPbapObexServer.sIsAborted = false; 721 break; 722 } 723 String vcard = composer.createOneEntry(); 724 if (vcard == null) { 725 Log.e(TAG, 726 "Failed to read a contact. Error reason: " + composer.getErrorReason()); 727 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 728 } 729 if (V) Log.v(TAG, "vCard from composer: " + vcard); 730 731 vcard = vcardfilter.apply(vcard, vcardType21); 732 vcard = StripTelephoneNumber(vcard); 733 734 if (V) Log.v(TAG, "vCard after cleanup: " + vcard); 735 736 if (!buffer.onEntryCreated(vcard)) { 737 // onEntryCreate() already emits error. 738 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 739 } 740 } 741 } finally { 742 if (composer != null) { 743 composer.terminate(); 744 } 745 if (buffer != null) { 746 buffer.onTerminate(); 747 } 748 } 749 750 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 751 + (System.currentTimeMillis() - timestamp) + " ms"); 752 753 return ResponseCodes.OBEX_HTTP_OK; 754 } 755 756 private final int composeContactsAndSendSelectedVCards(Operation op, 757 final Cursor contactIdCursor, final boolean vcardType21, String ownerVCard, 758 int needSendBody, int pbSize, boolean ignorefilter, byte[] filter, byte[] selector, 759 String vcardselectorop) { 760 long timestamp = 0; 761 if (V) timestamp = System.currentTimeMillis(); 762 763 VCardComposer composer = null; 764 VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter); 765 PropertySelector vcardselector = new PropertySelector(selector); 766 767 HandlerForStringBuffer buffer = null; 768 769 try { 770 // Currently only support Generic Vcard 2.1 and 3.0 771 int vcardType; 772 if (vcardType21) { 773 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 774 } else { 775 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 776 } 777 if (!vcardfilter.isPhotoEnabled()) { 778 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 779 } 780 781 // Enhancement: customize Vcard based on preferences/settings and 782 // input from caller 783 composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null); 784 // End enhancement 785 786 /* BT does want PAUSE/WAIT conversion while it doesn't want the 787 * other formatting done by vCard library by default. */ 788 composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() { 789 public String onValueReceived( 790 String rawValue, int type, String label, boolean isPrimary) { 791 /* 'p' and 'w' are the standard characters for pause and wait 792 * (see RFC 3601) so use those when exporting phone numbers via vCard.*/ 793 String numberWithControlSequence = rawValue.replace(PhoneNumberUtils.PAUSE, 'p') 794 .replace(PhoneNumberUtils.WAIT, 'w'); 795 return numberWithControlSequence; 796 } 797 }); 798 buffer = new HandlerForStringBuffer(op, ownerVCard); 799 Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount()); 800 if (!composer.initWithCallback( 801 contactIdCursor, new EnterpriseRawContactEntitlesInfoCallback()) 802 || !buffer.onInit(mContext)) { 803 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 804 } 805 806 while (!composer.isAfterLast()) { 807 if (BluetoothPbapObexServer.sIsAborted) { 808 ((ServerOperation) op).isAborted = true; 809 BluetoothPbapObexServer.sIsAborted = false; 810 break; 811 } 812 String vcard = composer.createOneEntry(); 813 if (vcard == null) { 814 Log.e(TAG, 815 "Failed to read a contact. Error reason: " + composer.getErrorReason()); 816 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 817 } 818 if (V) Log.v(TAG, "Checking selected bits in the vcard composer" + vcard); 819 820 if (!vcardselector.CheckVcardSelector(vcard, vcardselectorop)) { 821 Log.e(TAG, "vcard selector check fail"); 822 vcard = null; 823 pbSize--; 824 continue; 825 } 826 827 Log.e(TAG, "vcard selector check pass"); 828 829 if (needSendBody == NEED_SEND_BODY) { 830 vcard = vcardfilter.apply(vcard, vcardType21); 831 vcard = StripTelephoneNumber(vcard); 832 833 if (V) Log.v(TAG, "vCard after cleanup: " + vcard); 834 835 if (!buffer.onEntryCreated(vcard)) { 836 // onEntryCreate() already emits error. 837 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 838 } 839 } 840 } 841 842 if (needSendBody != NEED_SEND_BODY) return pbSize; 843 } finally { 844 if (composer != null) { 845 composer.terminate(); 846 } 847 if (buffer != null) { 848 buffer.onTerminate(); 849 } 850 } 851 852 if (V) 853 Log.v(TAG, "Total vcard composing and sending out takes " 854 + (System.currentTimeMillis() - timestamp) + " ms"); 855 856 return ResponseCodes.OBEX_HTTP_OK; 857 } 858 859 private final int composeCallLogsAndSendSelectedVCards(Operation op, final String selection, 860 final boolean vcardType21, int needSendBody, int pbSize, String ownerVCard, 861 boolean ignorefilter, byte[] filter, byte[] selector, String vcardselectorop, 862 boolean vCardSelct) { 863 long timestamp = 0; 864 if (V) timestamp = System.currentTimeMillis(); 865 866 BluetoothPbapCallLogComposer composer = null; 867 HandlerForStringBuffer buffer = null; 868 869 try { 870 VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter); 871 PropertySelector vcardselector = new PropertySelector(selector); 872 composer = new BluetoothPbapCallLogComposer(mContext); 873 buffer = new HandlerForStringBuffer(op, ownerVCard); 874 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER) 875 || !buffer.onInit(mContext)) { 876 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 877 } 878 879 while (!composer.isAfterLast()) { 880 if (BluetoothPbapObexServer.sIsAborted) { 881 ((ServerOperation) op).isAborted = true; 882 BluetoothPbapObexServer.sIsAborted = false; 883 break; 884 } 885 String vcard = composer.createOneEntry(vcardType21); 886 if (vCardSelct) { 887 if (!vcardselector.CheckVcardSelector(vcard, vcardselectorop)) { 888 Log.e(TAG, "Checking vcard selector for call log"); 889 vcard = null; 890 pbSize--; 891 continue; 892 } 893 if (needSendBody == NEED_SEND_BODY) { 894 if (vcard != null) { 895 vcard = vcardfilter.apply(vcard, vcardType21); 896 } 897 if (vcard == null) { 898 Log.e(TAG, "Failed to read a contact. Error reason: " 899 + composer.getErrorReason()); 900 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 901 } 902 if (V) { 903 Log.v(TAG, "Vcard Entry:"); 904 Log.v(TAG, vcard); 905 } 906 buffer.onEntryCreated(vcard); 907 } 908 } else { 909 if (vcard == null) { 910 Log.e(TAG, "Failed to read a contact. Error reason: " 911 + composer.getErrorReason()); 912 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 913 } 914 if (V) { 915 Log.v(TAG, "Vcard Entry:"); 916 Log.v(TAG, vcard); 917 } 918 buffer.onEntryCreated(vcard); 919 } 920 } 921 if (needSendBody != NEED_SEND_BODY && vCardSelct) return pbSize; 922 } finally { 923 if (composer != null) { 924 composer.terminate(); 925 } 926 if (buffer != null) { 927 buffer.onTerminate(); 928 } 929 } 930 931 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 932 + (System.currentTimeMillis() - timestamp) + " ms"); 933 return ResponseCodes.OBEX_HTTP_OK; 934 } 935 936 public String StripTelephoneNumber (String vCard){ 937 String attr [] = vCard.split(System.getProperty("line.separator")); 938 String Vcard = ""; 939 for (int i=0; i < attr.length; i++) { 940 if(attr[i].startsWith("TEL")) { 941 attr[i] = attr[i].replace("(", ""); 942 attr[i] = attr[i].replace(")", ""); 943 attr[i] = attr[i].replace("-", ""); 944 attr[i] = attr[i].replace(" ", ""); 945 } 946 } 947 948 for (int i=0; i < attr.length; i++) { 949 if(!attr[i].equals("")){ 950 Vcard = Vcard.concat(attr[i] + "\n"); 951 } 952 } 953 if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard); 954 return Vcard; 955 } 956 957 /** 958 * Handler to emit vCards to PCE. 959 */ 960 public class HandlerForStringBuffer { 961 private Operation operation; 962 963 private OutputStream outputStream; 964 965 private String phoneOwnVCard = null; 966 967 public HandlerForStringBuffer(Operation op, String ownerVCard) { 968 operation = op; 969 if (ownerVCard != null) { 970 phoneOwnVCard = ownerVCard; 971 if (V) Log.v(TAG, "phone own number vcard:"); 972 if (V) Log.v(TAG, phoneOwnVCard); 973 } 974 } 975 976 private boolean write(String vCard) { 977 try { 978 if (vCard != null) { 979 outputStream.write(vCard.getBytes()); 980 return true; 981 } 982 } catch (IOException e) { 983 Log.e(TAG, "write outputstrem failed" + e.toString()); 984 } 985 return false; 986 } 987 988 public boolean onInit(Context context) { 989 try { 990 outputStream = operation.openOutputStream(); 991 if (phoneOwnVCard != null) { 992 return write(phoneOwnVCard); 993 } 994 return true; 995 } catch (IOException e) { 996 Log.e(TAG, "open outputstrem failed" + e.toString()); 997 } 998 return false; 999 } 1000 1001 public boolean onEntryCreated(String vcard) { 1002 return write(vcard); 1003 } 1004 1005 public void onTerminate() { 1006 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 1007 if (V) Log.v(TAG, "CloseStream failed!"); 1008 } else { 1009 if (V) Log.v(TAG, "CloseStream ok!"); 1010 } 1011 } 1012 } 1013 1014 public static class VCardFilter { 1015 private static enum FilterBit { 1016 // bit property onlyCheckV21 excludeForV21 1017 FN ( 1, "FN", true, false), 1018 PHOTO( 3, "PHOTO", false, false), 1019 BDAY( 4, "BDAY", false, false), 1020 ADR( 5, "ADR", false, false), 1021 EMAIL( 8, "EMAIL", false, false), 1022 TITLE( 12, "TITLE", false, false), 1023 ORG( 16, "ORG", false, false), 1024 NOTE( 17, "NOTE", false, false), 1025 URL( 20, "URL", false, false), 1026 NICKNAME( 23, "NICKNAME", false, true), 1027 DATETIME( 28, "X-IRMC-CALL-DATETIME", false, false); 1028 1029 public final int pos; 1030 public final String prop; 1031 public final boolean onlyCheckV21; 1032 public final boolean excludeForV21; 1033 1034 FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) { 1035 this.pos = pos; 1036 this.prop = prop; 1037 this.onlyCheckV21 = onlyCheckV21; 1038 this.excludeForV21 = excludeForV21; 1039 } 1040 } 1041 1042 private static final String SEPARATOR = System.getProperty("line.separator"); 1043 private final byte[] filter; 1044 1045 //This function returns true if the attributes needs to be included in the filtered vcard. 1046 private boolean isFilteredIn(FilterBit bit, boolean vCardType21) { 1047 final int offset = (bit.pos / 8) + 1; 1048 final int bit_pos = bit.pos % 8; 1049 if (!vCardType21 && bit.onlyCheckV21) return true; 1050 if (vCardType21 && bit.excludeForV21) return false; 1051 if (filter == null || offset >= filter.length) return true; 1052 return ((filter[filter.length - offset] >> bit_pos) & 0x01) != 0; 1053 } 1054 1055 VCardFilter(byte[] filter) { 1056 this.filter = filter; 1057 } 1058 1059 public boolean isPhotoEnabled() { 1060 return isFilteredIn(FilterBit.PHOTO, false); 1061 } 1062 1063 public String apply(String vCard, boolean vCardType21){ 1064 if (filter == null) return vCard; 1065 String lines[] = vCard.split(SEPARATOR); 1066 StringBuilder filteredVCard = new StringBuilder(); 1067 boolean filteredIn = false; 1068 1069 for (String line : lines) { 1070 // Check whether the current property is changing (ignoring multi-line properties) 1071 // and determine if the current property is filtered in. 1072 if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) { 1073 String currentProp = line.split("[;:]")[0]; 1074 filteredIn = true; 1075 1076 for (FilterBit bit : FilterBit.values()) { 1077 if (bit.prop.equals(currentProp)) { 1078 filteredIn = isFilteredIn(bit, vCardType21); 1079 break; 1080 } 1081 } 1082 1083 // Since PBAP does not have filter bits for IM and SIP, 1084 // exclude them by default. Easiest way is to exclude all 1085 // X- fields, except date time.... 1086 if (currentProp.startsWith("X-")) { 1087 filteredIn = false; 1088 if (currentProp.equals("X-IRMC-CALL-DATETIME")) { 1089 filteredIn = true; 1090 } 1091 } 1092 } 1093 1094 // Build filtered vCard 1095 if (filteredIn) { 1096 filteredVCard.append(line + SEPARATOR); 1097 } 1098 } 1099 1100 return filteredVCard.toString(); 1101 } 1102 } 1103 1104 private static class PropertySelector { 1105 private static enum PropertyMask { 1106 // bit property 1107 VERSION(0, "VERSION"), 1108 FN(1, "FN"), 1109 NAME(2, "N"), 1110 PHOTO(3, "PHOTO"), 1111 BDAY(4, "BDAY"), 1112 ADR(5, "ADR"), 1113 LABEL(6, "LABEL"), 1114 TEL(7, "TEL"), 1115 EMAIL(8, "EMAIL"), 1116 TITLE(12, "TITLE"), 1117 ORG(16, "ORG"), 1118 NOTE(17, "NOTE"), 1119 URL(20, "URL"), 1120 NICKNAME(23, "NICKNAME"), 1121 DATETIME(28, "DATETIME"); 1122 1123 public final int pos; 1124 public final String prop; 1125 1126 PropertyMask(int pos, String prop) { 1127 this.pos = pos; 1128 this.prop = prop; 1129 } 1130 } 1131 1132 private static final String SEPARATOR = System.getProperty("line.separator"); 1133 private final byte[] selector; 1134 1135 PropertySelector(byte[] selector) { 1136 this.selector = selector; 1137 } 1138 1139 private boolean checkbit(int attr_bit, byte[] selector) { 1140 int selectorlen = selector.length; 1141 if (((selector[selectorlen - 1 - ((int) attr_bit / 8)] >> (attr_bit % 8)) & 0x01) 1142 == 0) { 1143 return false; 1144 } 1145 return true; 1146 } 1147 1148 private boolean checkprop(String vcard, String prop) { 1149 String lines[] = vcard.split(SEPARATOR); 1150 boolean isPresent = false; 1151 for (String line : lines) { 1152 if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) { 1153 String currentProp = line.split("[;:]")[0]; 1154 if (prop.equals(currentProp)) { 1155 Log.d(TAG, "bit.prop.equals current prop :" + prop); 1156 isPresent = true; 1157 return isPresent; 1158 } 1159 } 1160 } 1161 1162 return isPresent; 1163 } 1164 1165 private boolean CheckVcardSelector(String vcard, String vcardselectorop) { 1166 boolean selectedIn = true; 1167 1168 for (PropertyMask bit : PropertyMask.values()) { 1169 if (checkbit(bit.pos, selector)) { 1170 Log.d(TAG, "checking for prop :" + bit.prop); 1171 if (vcardselectorop.equals("0")) { 1172 if (checkprop(vcard, bit.prop)) { 1173 Log.d(TAG, "bit.prop.equals current prop :" + bit.prop); 1174 selectedIn = true; 1175 break; 1176 } else { 1177 selectedIn = false; 1178 } 1179 } else if (vcardselectorop.equals("1")) { 1180 if (!checkprop(vcard, bit.prop)) { 1181 Log.d(TAG, "bit.prop.notequals current prop" + bit.prop); 1182 selectedIn = false; 1183 return selectedIn; 1184 } else { 1185 selectedIn = true; 1186 } 1187 } 1188 } 1189 } 1190 return selectedIn; 1191 } 1192 1193 private String getName(String vcard) { 1194 String lines[] = vcard.split(SEPARATOR); 1195 String name = ""; 1196 for (String line : lines) { 1197 if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) { 1198 if (line.startsWith("N:")) 1199 name = line.substring(line.lastIndexOf(':'), line.length()); 1200 } 1201 } 1202 Log.d(TAG, "returning name: " + name); 1203 return name; 1204 } 1205 } 1206 1207 private static final Uri getPhoneLookupFilterUri() { 1208 return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI; 1209 } 1210 1211 /** 1212 * Get size of the cursor without duplicated contact id. This assumes the 1213 * given cursor is sorted by CONTACT_ID. 1214 */ 1215 private static final int getDistinctContactIdSize(Cursor cursor) { 1216 final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); 1217 final int idColumn = cursor.getColumnIndex(Data._ID); 1218 long previousContactId = -1; 1219 int count = 0; 1220 cursor.moveToPosition(-1); 1221 while (cursor.moveToNext()) { 1222 final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn); 1223 if (previousContactId != contactId) { 1224 count++; 1225 previousContactId = contactId; 1226 } 1227 } 1228 if (V) { 1229 Log.i(TAG, "getDistinctContactIdSize result: " + count); 1230 } 1231 return count; 1232 } 1233 1234 /** 1235 * Append "display_name,contact_id" string array from cursor to ArrayList. 1236 * This assumes the given cursor is sorted by CONTACT_ID. 1237 */ 1238 private static void appendDistinctNameIdList(ArrayList<String> resultList, 1239 String defaultName, Cursor cursor) { 1240 final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); 1241 final int idColumn = cursor.getColumnIndex(Data._ID); 1242 final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME); 1243 cursor.moveToPosition(-1); 1244 while (cursor.moveToNext()) { 1245 final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn); 1246 String displayName = nameColumn != -1 ? cursor.getString(nameColumn) : defaultName; 1247 if (TextUtils.isEmpty(displayName)) { 1248 displayName = defaultName; 1249 } 1250 1251 String newString = displayName + "," + contactId; 1252 if (!resultList.contains(newString)) { 1253 resultList.add(newString); 1254 } 1255 } 1256 if (V) { 1257 for (String nameId : resultList) { 1258 Log.i(TAG, "appendDistinctNameIdList result: " + nameId); 1259 } 1260 } 1261 } 1262} 1263