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