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