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.CursorWindowAllocationException; 39 import android.database.Cursor; 40 import android.net.Uri; 41 import android.provider.CallLog; 42 import android.provider.CallLog.Calls; 43 import android.provider.ContactsContract.CommonDataKinds; 44 import android.provider.ContactsContract.Contacts; 45 import android.provider.ContactsContract.Data; 46 import android.provider.ContactsContract.CommonDataKinds.Phone; 47 import android.provider.ContactsContract.PhoneLookup; 48 import android.telephony.PhoneNumberUtils; 49 import android.text.TextUtils; 50 import android.util.Log; 51 52 import com.android.bluetooth.R; 53 import com.android.vcard.VCardComposer; 54 import com.android.vcard.VCardConfig; 55 import com.android.vcard.VCardPhoneNumberTranslationCallback; 56 57 import java.io.IOException; 58 import java.io.OutputStream; 59 import java.util.ArrayList; 60 61 import javax.obex.ServerOperation; 62 import javax.obex.Operation; 63 import javax.obex.ResponseCodes; 64 65 import com.android.bluetooth.Utils; 66 67 public class BluetoothPbapVcardManager { 68 private static final String TAG = "BluetoothPbapVcardManager"; 69 70 private static final boolean V = BluetoothPbapService.VERBOSE; 71 72 private ContentResolver mResolver; 73 74 private Context mContext; 75 76 static final String[] PHONES_PROJECTION = new String[] { 77 Data._ID, // 0 78 CommonDataKinds.Phone.TYPE, // 1 79 CommonDataKinds.Phone.LABEL, // 2 80 CommonDataKinds.Phone.NUMBER, // 3 81 Contacts.DISPLAY_NAME, // 4 82 }; 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[] CONTACTS_PROJECTION = new String[] { 89 Contacts._ID, // 0 90 Contacts.DISPLAY_NAME, // 1 91 }; 92 93 static final int CONTACTS_ID_COLUMN_INDEX = 0; 94 95 static final int CONTACTS_NAME_COLUMN_INDEX = 1; 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 String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; 104 BluetoothPbapVcardManager(final Context context)105 public BluetoothPbapVcardManager(final Context context) { 106 mContext = context; 107 mResolver = mContext.getContentResolver(); 108 } 109 110 /** 111 * Create an owner vcard from the configured profile 112 * @param vcardType21 113 * @return 114 */ getOwnerPhoneNumberVcardFromProfile(final boolean vcardType21, final byte[] filter)115 private final String getOwnerPhoneNumberVcardFromProfile(final boolean vcardType21, final byte[] filter) { 116 // Currently only support Generic Vcard 2.1 and 3.0 117 int vcardType; 118 if (vcardType21) { 119 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 120 } else { 121 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 122 } 123 124 if (!BluetoothPbapConfig.includePhotosInVcard()) { 125 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 126 } 127 128 return BluetoothPbapUtils.createProfileVCard(mContext, vcardType,filter); 129 } 130 getOwnerPhoneNumberVcard(final boolean vcardType21, final byte[] filter)131 public final String getOwnerPhoneNumberVcard(final boolean vcardType21, final byte[] filter) { 132 //Owner vCard enhancement: Use "ME" profile if configured 133 if (BluetoothPbapConfig.useProfileForOwnerVcard()) { 134 String vcard = getOwnerPhoneNumberVcardFromProfile(vcardType21, filter); 135 if (vcard != null && vcard.length() != 0) { 136 return vcard; 137 } 138 } 139 //End enhancement 140 141 BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext); 142 String name = BluetoothPbapService.getLocalPhoneName(); 143 String number = BluetoothPbapService.getLocalPhoneNum(); 144 String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number, 145 vcardType21); 146 return vcard; 147 } 148 getPhonebookSize(final int type)149 public final int getPhonebookSize(final int type) { 150 int size; 151 switch (type) { 152 case BluetoothPbapObexServer.ContentType.PHONEBOOK: 153 size = getContactsSize(); 154 break; 155 default: 156 size = getCallHistorySize(type); 157 break; 158 } 159 if (V) Log.v(TAG, "getPhonebookSize size = " + size + " type = " + type); 160 return size; 161 } 162 getContactsSize()163 public final int getContactsSize() { 164 final Uri myUri = Contacts.CONTENT_URI; 165 int size = 0; 166 Cursor contactCursor = null; 167 try { 168 contactCursor = mResolver.query(myUri, null, CLAUSE_ONLY_VISIBLE, null, null); 169 if (contactCursor != null) { 170 size = contactCursor.getCount() + 1; // always has the 0.vcf 171 } 172 } catch (CursorWindowAllocationException e) { 173 Log.e(TAG, "CursorWindowAllocationException while getting Contacts size"); 174 } finally { 175 if (contactCursor != null) { 176 contactCursor.close(); 177 contactCursor = null; 178 } 179 } 180 return size; 181 } 182 getCallHistorySize(final int type)183 public final int getCallHistorySize(final int type) { 184 final Uri myUri = CallLog.Calls.CONTENT_URI; 185 String selection = BluetoothPbapObexServer.createSelectionPara(type); 186 int size = 0; 187 Cursor callCursor = null; 188 try { 189 callCursor = mResolver.query(myUri, null, selection, null, 190 CallLog.Calls.DEFAULT_SORT_ORDER); 191 if (callCursor != null) { 192 size = callCursor.getCount(); 193 } 194 } catch (CursorWindowAllocationException e) { 195 Log.e(TAG, "CursorWindowAllocationException while getting CallHistory size"); 196 } finally { 197 if (callCursor != null) { 198 callCursor.close(); 199 callCursor = null; 200 } 201 } 202 return size; 203 } 204 loadCallHistoryList(final int type)205 public final ArrayList<String> loadCallHistoryList(final int type) { 206 final Uri myUri = CallLog.Calls.CONTENT_URI; 207 String selection = BluetoothPbapObexServer.createSelectionPara(type); 208 String[] projection = new String[] { 209 Calls.NUMBER, Calls.CACHED_NAME, Calls.NUMBER_PRESENTATION 210 }; 211 final int CALLS_NUMBER_COLUMN_INDEX = 0; 212 final int CALLS_NAME_COLUMN_INDEX = 1; 213 final int CALLS_NUMBER_PRESENTATION_COLUMN_INDEX = 2; 214 215 Cursor callCursor = null; 216 ArrayList<String> list = new ArrayList<String>(); 217 try { 218 callCursor = mResolver.query(myUri, projection, selection, null, 219 CALLLOG_SORT_ORDER); 220 if (callCursor != null) { 221 for (callCursor.moveToFirst(); !callCursor.isAfterLast(); 222 callCursor.moveToNext()) { 223 String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX); 224 if (TextUtils.isEmpty(name)) { 225 // name not found, use number instead 226 final int numberPresentation = callCursor.getInt( 227 CALLS_NUMBER_PRESENTATION_COLUMN_INDEX); 228 if (numberPresentation != Calls.PRESENTATION_ALLOWED) { 229 name = mContext.getString(R.string.unknownNumber); 230 } else { 231 name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX); 232 } 233 } 234 list.add(name); 235 } 236 } 237 } catch (CursorWindowAllocationException e) { 238 Log.e(TAG, "CursorWindowAllocationException while loading CallHistory"); 239 } finally { 240 if (callCursor != null) { 241 callCursor.close(); 242 callCursor = null; 243 } 244 } 245 return list; 246 } 247 getPhonebookNameList(final int orderByWhat)248 public final ArrayList<String> getPhonebookNameList(final int orderByWhat) { 249 ArrayList<String> nameList = new ArrayList<String>(); 250 //Owner vCard enhancement. Use "ME" profile if configured 251 String ownerName = null; 252 if (BluetoothPbapConfig.useProfileForOwnerVcard()) { 253 ownerName = BluetoothPbapUtils.getProfileName(mContext); 254 } 255 if (ownerName == null || ownerName.length()==0) { 256 ownerName = BluetoothPbapService.getLocalPhoneName(); 257 } 258 nameList.add(ownerName); 259 //End enhancement 260 261 final Uri myUri = Contacts.CONTENT_URI; 262 Cursor contactCursor = null; 263 try { 264 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 265 if (V) Log.v(TAG, "getPhonebookNameList, order by index"); 266 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 267 null, Contacts._ID); 268 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 269 if (V) Log.v(TAG, "getPhonebookNameList, order by alpha"); 270 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 271 null, Contacts.DISPLAY_NAME); 272 } 273 if (contactCursor != null) { 274 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor 275 .moveToNext()) { 276 String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX); 277 long id = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 278 if (TextUtils.isEmpty(name)) { 279 name = mContext.getString(android.R.string.unknownName); 280 } 281 nameList.add(name + "," + id); 282 } 283 } 284 } catch (CursorWindowAllocationException e) { 285 Log.e(TAG, "CursorWindowAllocationException while getting Phonebook name list"); 286 } finally { 287 if (contactCursor != null) { 288 contactCursor.close(); 289 contactCursor = null; 290 } 291 } 292 return nameList; 293 } 294 getContactNamesByNumber(final String phoneNumber)295 public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) { 296 ArrayList<String> nameList = new ArrayList<String>(); 297 ArrayList<String> tempNameList = new ArrayList<String>(); 298 299 Cursor contactCursor = null; 300 Uri uri = null; 301 302 if (phoneNumber != null && phoneNumber.length() == 0) { 303 uri = Contacts.CONTENT_URI; 304 } else { 305 uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, 306 Uri.encode(phoneNumber)); 307 } 308 309 try { 310 contactCursor = mResolver.query(uri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 311 null, Contacts._ID); 312 313 if (contactCursor != null) { 314 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor 315 .moveToNext()) { 316 String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX); 317 long id = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 318 if (TextUtils.isEmpty(name)) { 319 name = mContext.getString(android.R.string.unknownName); 320 } 321 if (V) Log.v(TAG, "got name " + name + " by number " + phoneNumber + " @" + id); 322 tempNameList.add(name + "," + id); 323 } 324 } 325 } catch (CursorWindowAllocationException e) { 326 Log.e(TAG, "CursorWindowAllocationException while getting contact names"); 327 } finally { 328 if (contactCursor != null) { 329 contactCursor.close(); 330 contactCursor = null; 331 } 332 } 333 int tempListSize = tempNameList.size(); 334 for (int index = 0; index < tempListSize; index++) { 335 String object = tempNameList.get(index); 336 if (!nameList.contains(object)) 337 nameList.add(object); 338 } 339 340 return nameList; 341 } 342 composeAndSendCallLogVcards(final int type, Operation op, final int startPoint, final int endPoint, final boolean vcardType21, boolean ignorefilter, byte[] filter)343 public final int composeAndSendCallLogVcards(final int type, Operation op, 344 final int startPoint, final int endPoint, final boolean vcardType21, 345 boolean ignorefilter, byte[] filter) { 346 if (startPoint < 1 || startPoint > endPoint) { 347 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 348 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 349 } 350 String typeSelection = BluetoothPbapObexServer.createSelectionPara(type); 351 352 final Uri myUri = CallLog.Calls.CONTENT_URI; 353 final String[] CALLLOG_PROJECTION = new String[] { 354 CallLog.Calls._ID, // 0 355 }; 356 final int ID_COLUMN_INDEX = 0; 357 358 Cursor callsCursor = null; 359 long startPointId = 0; 360 long endPointId = 0; 361 try { 362 // Need test to see if order by _ID is ok here, or by date? 363 callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null, 364 CALLLOG_SORT_ORDER); 365 if (callsCursor != null) { 366 callsCursor.moveToPosition(startPoint - 1); 367 startPointId = callsCursor.getLong(ID_COLUMN_INDEX); 368 if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId); 369 if (startPoint == endPoint) { 370 endPointId = startPointId; 371 } else { 372 callsCursor.moveToPosition(endPoint - 1); 373 endPointId = callsCursor.getLong(ID_COLUMN_INDEX); 374 } 375 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId); 376 } 377 } catch (CursorWindowAllocationException e) { 378 Log.e(TAG, "CursorWindowAllocationException while composing calllog vcards"); 379 } finally { 380 if (callsCursor != null) { 381 callsCursor.close(); 382 callsCursor = null; 383 } 384 } 385 386 String recordSelection; 387 if (startPoint == endPoint) { 388 recordSelection = Calls._ID + "=" + startPointId; 389 } else { 390 // The query to call table is by "_id DESC" order, so change 391 // correspondingly. 392 recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<=" 393 + startPointId; 394 } 395 396 String selection; 397 if (typeSelection == null) { 398 selection = recordSelection; 399 } else { 400 selection = "(" + typeSelection + ") AND (" + recordSelection + ")"; 401 } 402 403 if (V) Log.v(TAG, "Call log query selection is: " + selection); 404 405 return composeAndSendVCards(op, selection, vcardType21, null, false, ignorefilter, filter); 406 } 407 composeAndSendPhonebookVcards(Operation op, final int startPoint, final int endPoint, final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter)408 public final int composeAndSendPhonebookVcards(Operation op, final int startPoint, 409 final int endPoint, final boolean vcardType21, String ownerVCard, 410 boolean ignorefilter, byte[] filter) { 411 if (startPoint < 1 || startPoint > endPoint) { 412 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 413 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 414 } 415 final Uri myUri = Contacts.CONTENT_URI; 416 417 Cursor contactCursor = null; 418 long startPointId = 0; 419 long endPointId = 0; 420 try { 421 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, null, 422 Contacts._ID); 423 if (contactCursor != null) { 424 contactCursor.moveToPosition(startPoint - 1); 425 startPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 426 if (V) Log.v(TAG, "Query startPointId = " + startPointId); 427 if (startPoint == endPoint) { 428 endPointId = startPointId; 429 } else { 430 contactCursor.moveToPosition(endPoint - 1); 431 endPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 432 } 433 if (V) Log.v(TAG, "Query endPointId = " + endPointId); 434 } 435 } catch (CursorWindowAllocationException e) { 436 Log.e(TAG, "CursorWindowAllocationException while composing phonebook vcards"); 437 } finally { 438 if (contactCursor != null) { 439 contactCursor.close(); 440 contactCursor = null; 441 } 442 } 443 444 final String selection; 445 if (startPoint == endPoint) { 446 selection = Contacts._ID + "=" + startPointId + " AND " + CLAUSE_ONLY_VISIBLE; 447 } else { 448 selection = Contacts._ID + ">=" + startPointId + " AND " + Contacts._ID + "<=" 449 + endPointId + " AND " + CLAUSE_ONLY_VISIBLE; 450 } 451 452 if (V) Log.v(TAG, "Query selection is: " + selection); 453 454 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true, 455 ignorefilter, filter); 456 } 457 composeAndSendPhonebookOneVcard(Operation op, final int offset, final boolean vcardType21, String ownerVCard, int orderByWhat, boolean ignorefilter, byte[] filter)458 public final int composeAndSendPhonebookOneVcard(Operation op, final int offset, 459 final boolean vcardType21, String ownerVCard, int orderByWhat, 460 boolean ignorefilter, byte[] filter) { 461 if (offset < 1) { 462 Log.e(TAG, "Internal error: offset is not correct."); 463 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 464 } 465 final Uri myUri = Contacts.CONTENT_URI; 466 Cursor contactCursor = null; 467 String selection = null; 468 long contactId = 0; 469 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 470 try { 471 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 472 null, Contacts._ID); 473 if (contactCursor != null) { 474 contactCursor.moveToPosition(offset - 1); 475 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 476 if (V) Log.v(TAG, "Query startPointId = " + contactId); 477 } 478 } catch (CursorWindowAllocationException e) { 479 Log.e(TAG, "CursorWindowAllocationException while composing phonebook one vcard order by index"); 480 } finally { 481 if (contactCursor != null) { 482 contactCursor.close(); 483 contactCursor = null; 484 } 485 } 486 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 487 try { 488 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 489 null, Contacts.DISPLAY_NAME); 490 if (contactCursor != null) { 491 contactCursor.moveToPosition(offset - 1); 492 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 493 if (V) Log.v(TAG, "Query startPointId = " + contactId); 494 } 495 } catch (CursorWindowAllocationException e) { 496 Log.e(TAG, "CursorWindowAllocationException while composing phonebook one vcard order by alphabetical"); 497 } finally { 498 if (contactCursor != null) { 499 contactCursor.close(); 500 contactCursor = null; 501 } 502 } 503 } else { 504 Log.e(TAG, "Parameter orderByWhat is not supported!"); 505 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 506 } 507 selection = Contacts._ID + "=" + contactId; 508 509 if (V) Log.v(TAG, "Query selection is: " + selection); 510 511 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true, 512 ignorefilter, filter); 513 } 514 composeAndSendVCards(Operation op, final String selection, final boolean vcardType21, String ownerVCard, boolean isContacts, boolean ignorefilter, byte[] filter)515 public final int composeAndSendVCards(Operation op, final String selection, 516 final boolean vcardType21, String ownerVCard, boolean isContacts, 517 boolean ignorefilter, byte[] filter) { 518 long timestamp = 0; 519 if (V) timestamp = System.currentTimeMillis(); 520 521 if (isContacts) { 522 VCardComposer composer = null; 523 FilterVcard vcardfilter= new FilterVcard(); 524 if (!ignorefilter) { 525 vcardfilter.setFilter(filter); 526 } 527 HandlerForStringBuffer buffer = null; 528 try { 529 // Currently only support Generic Vcard 2.1 and 3.0 530 int vcardType; 531 if (vcardType21) { 532 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 533 } else { 534 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 535 } 536 if (!vcardfilter.isPhotoEnabled()) { 537 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 538 } 539 540 //Enhancement: customize Vcard based on preferences/settings and input from caller 541 composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType,null); 542 //End enhancement 543 544 // BT does want PAUSE/WAIT conversion while it doesn't want the other formatting 545 // done by vCard library by default. 546 composer.setPhoneNumberTranslationCallback( 547 new VCardPhoneNumberTranslationCallback() { 548 public String onValueReceived( 549 String rawValue, int type, String label, boolean isPrimary) { 550 // 'p' and 'w' are the standard characters for pause and wait 551 // (see RFC 3601) 552 // so use those when exporting phone numbers via vCard. 553 String numberWithControlSequence = rawValue 554 .replace(PhoneNumberUtils.PAUSE, 'p') 555 .replace(PhoneNumberUtils.WAIT, 'w'); 556 return numberWithControlSequence; 557 } 558 }); 559 buffer = new HandlerForStringBuffer(op, ownerVCard); 560 if (!composer.init(Contacts.CONTENT_URI, selection, null, Contacts._ID) || 561 !buffer.onInit(mContext)) { 562 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 563 } 564 565 while (!composer.isAfterLast()) { 566 if (BluetoothPbapObexServer.sIsAborted) { 567 ((ServerOperation)op).isAborted = true; 568 BluetoothPbapObexServer.sIsAborted = false; 569 break; 570 } 571 String vcard = composer.createOneEntry(); 572 if (vcard == null) { 573 Log.e(TAG, "Failed to read a contact. Error reason: " 574 + composer.getErrorReason()); 575 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 576 } 577 if (V) Log.v (TAG , "vCard from composer: " + vcard); 578 if (!ignorefilter) { 579 vcard = vcardfilter.applyFilter(vcard, vcardType21); 580 if (V) Log.v (TAG , "vCard on applying filter: " + vcard); 581 } 582 vcard = StripTelephoneNumber(vcard); 583 if (V) { 584 Log.v(TAG, "Vcard Entry:"); 585 Log.v(TAG,vcard); 586 } 587 588 if (!buffer.onEntryCreated(vcard)) { 589 // onEntryCreate() already emits error. 590 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 591 } 592 } 593 } finally { 594 if (composer != null) { 595 composer.terminate(); 596 } 597 if (buffer != null) { 598 buffer.onTerminate(); 599 } 600 } 601 } else { // CallLog 602 BluetoothPbapCallLogComposer composer = null; 603 HandlerForStringBuffer buffer = null; 604 try { 605 606 composer = new BluetoothPbapCallLogComposer(mContext); 607 buffer = new HandlerForStringBuffer(op, ownerVCard); 608 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, 609 CALLLOG_SORT_ORDER) || 610 !buffer.onInit(mContext)) { 611 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 612 } 613 614 while (!composer.isAfterLast()) { 615 if (BluetoothPbapObexServer.sIsAborted) { 616 ((ServerOperation)op).isAborted = true; 617 BluetoothPbapObexServer.sIsAborted = false; 618 break; 619 } 620 String vcard = composer.createOneEntry(vcardType21); 621 if (vcard == null) { 622 Log.e(TAG, "Failed to read a contact. Error reason: " 623 + composer.getErrorReason()); 624 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 625 } 626 if (V) { 627 Log.v(TAG, "Vcard Entry:"); 628 Log.v(TAG,vcard); 629 } 630 631 buffer.onEntryCreated(vcard); 632 } 633 } finally { 634 if (composer != null) { 635 composer.terminate(); 636 } 637 if (buffer != null) { 638 buffer.onTerminate(); 639 } 640 } 641 } 642 643 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 644 + (System.currentTimeMillis() - timestamp) + " ms"); 645 646 return ResponseCodes.OBEX_HTTP_OK; 647 } 648 StripTelephoneNumber(String vCard)649 public String StripTelephoneNumber (String vCard){ 650 String attr [] = vCard.split(System.getProperty("line.separator")); 651 String Vcard = ""; 652 for (int i=0; i < attr.length; i++) { 653 if(attr[i].startsWith("TEL")) { 654 attr[i] = attr[i].replace("(", ""); 655 attr[i] = attr[i].replace(")", ""); 656 attr[i] = attr[i].replace("-", ""); 657 attr[i] = attr[i].replace(" ", ""); 658 } 659 } 660 661 for (int i=0; i < attr.length; i++) { 662 if(!attr[i].equals("")){ 663 Vcard = Vcard.concat(attr[i] + "\n"); 664 } 665 } 666 if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard); 667 return Vcard; 668 } 669 670 /** 671 * Handler to emit vCards to PCE. 672 */ 673 public class HandlerForStringBuffer { 674 private Operation operation; 675 676 private OutputStream outputStream; 677 678 private String phoneOwnVCard = null; 679 HandlerForStringBuffer(Operation op, String ownerVCard)680 public HandlerForStringBuffer(Operation op, String ownerVCard) { 681 operation = op; 682 if (ownerVCard != null) { 683 phoneOwnVCard = ownerVCard; 684 if (V) Log.v(TAG, "phone own number vcard:"); 685 if (V) Log.v(TAG, phoneOwnVCard); 686 } 687 } 688 write(String vCard)689 private boolean write(String vCard) { 690 try { 691 if (vCard != null) { 692 outputStream.write(vCard.getBytes()); 693 return true; 694 } 695 } catch (IOException e) { 696 Log.e(TAG, "write outputstrem failed" + e.toString()); 697 } 698 return false; 699 } 700 onInit(Context context)701 public boolean onInit(Context context) { 702 try { 703 outputStream = operation.openOutputStream(); 704 if (phoneOwnVCard != null) { 705 return write(phoneOwnVCard); 706 } 707 return true; 708 } catch (IOException e) { 709 Log.e(TAG, "open outputstrem failed" + e.toString()); 710 } 711 return false; 712 } 713 onEntryCreated(String vcard)714 public boolean onEntryCreated(String vcard) { 715 return write(vcard); 716 } 717 onTerminate()718 public void onTerminate() { 719 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 720 if (V) Log.v(TAG, "CloseStream failed!"); 721 } else { 722 if (V) Log.v(TAG, "CloseStream ok!"); 723 } 724 } 725 } 726 727 public class FilterVcard{ 728 FilterVcard()729 public FilterVcard(){ 730 }; 731 732 private final int FN_BIT = 1; 733 734 private boolean fn = true; 735 736 private final int PHOTO_BIT = 3; 737 738 private boolean photo = true; 739 740 //BDAY falls under events 741 private final int BDAY_BIT = 4; 742 743 private boolean bday = true; 744 745 private final int ADR_BIT = 5; 746 747 private boolean adr = true; 748 749 private final int EMAIL_BIT = 8; 750 751 private boolean email = true; 752 753 private final int TITLE_BIT = 12; 754 755 private boolean title = true; 756 757 private final int ORG_BIT = 16; 758 759 private boolean org = true; 760 761 private final int NOTES_BIT = 17; 762 763 private boolean notes = true; 764 765 private final int URL_BIT = 20; 766 767 private boolean url = true; 768 769 private final int NICKNAME_BIT = 23; 770 771 private boolean nickname = true; 772 setFilter(byte[] filter)773 public void setFilter(byte[] filter){ 774 775 fn = checkbit(FN_BIT, filter); 776 photo = checkbit(PHOTO_BIT, filter); 777 bday = checkbit(BDAY_BIT, filter); 778 adr = checkbit(ADR_BIT, filter); 779 email = checkbit(EMAIL_BIT, filter); 780 title = checkbit(TITLE_BIT, filter); 781 org = checkbit(ORG_BIT, filter); 782 notes = checkbit(NOTES_BIT, filter); 783 url = checkbit(URL_BIT, filter); 784 nickname = checkbit(NICKNAME_BIT, filter); 785 } 786 checkbit(int attr_bit, byte[] filter)787 private boolean checkbit (int attr_bit, byte[] filter){ 788 int filterlen = filter.length; 789 if( ((filter[filterlen -1 -((int)attr_bit/8)] >> (attr_bit%8)) & 0x01) == 0) { 790 return false; 791 } 792 return true; 793 } 794 isPhotoEnabled()795 public boolean isPhotoEnabled(){ 796 return photo; 797 } 798 checkValidFilter(String attr)799 private boolean checkValidFilter (String attr) { 800 if((attr.startsWith("N:")) || (attr.startsWith("TEL")) 801 || (attr.startsWith("VERSION")) || (attr.startsWith("URL")) 802 || (attr.startsWith("FN")) || (attr.startsWith("BDAY")) 803 || (attr.startsWith("ADR")) || (attr.startsWith("EMAIL")) 804 || (attr.startsWith("TITLE")) || (attr.startsWith("ORG")) 805 || (attr.startsWith("NOTE")) || (attr.startsWith("NICKNAME"))) { 806 return true; 807 } 808 return false; 809 } 810 applyFilter( String vCard, boolean vCardType21)811 public String applyFilter ( String vCard, boolean vCardType21){ 812 String attr [] = vCard.split(System.getProperty("line.separator")); 813 String filteredVcard = ""; 814 815 //FN is not the mandatory field in 2.1 vCard 816 if(((!fn) && (vCardType21)) && (vCard.contains("FN"))) { 817 for (int i=0; i < attr.length; i++) { 818 if(attr[i].startsWith("FN")){ 819 attr[i] = ""; 820 /** Remove multiline Content, if any */ 821 /** End traversal before END:VCARD */ 822 for (int j = i+1; j < attr.length - 1; j++) { 823 if (checkValidFilter(attr[j])) { 824 break; 825 } else { 826 /** Continuation of above attribute, remove */ 827 attr[j] = ""; 828 } 829 } 830 } 831 } 832 } 833 834 //NOTE: No need to check photo, we already refrained it if it is not set in the filter 835 if((!bday) && (vCard.contains("BDAY"))) { 836 for (int i=0; i < attr.length; i++) { 837 if(attr[i].startsWith("BDAY")){ 838 attr[i] = ""; 839 /** Remove multiline Content, if any */ 840 /** End traversal before END:VCARD */ 841 for (int j = i+1; j < attr.length - 1; j++) { 842 if (checkValidFilter(attr[j])) { 843 break; 844 } else { 845 /** Continuation of above attribute, remove */ 846 attr[j] = ""; 847 } 848 } 849 } 850 } 851 } 852 853 if((!adr) && (vCard.contains("ADR"))) { 854 for (int i=0; i < attr.length; i++) { 855 if(attr[i].startsWith("ADR")){ 856 attr[i] = ""; 857 /** Remove multiline Content, if any */ 858 /** End traversal before END:VCARD */ 859 for (int j = i+1; j < attr.length - 1; j++) { 860 if (checkValidFilter(attr[j])) { 861 break; 862 } else { 863 /** Continuation of above attribute, remove */ 864 attr[j] = ""; 865 } 866 } 867 } 868 } 869 } 870 871 if((!email) && (vCard.contains("EMAIL"))) { 872 for (int i=0; i < attr.length; i++) { 873 if(attr[i].startsWith("EMAIL")){ 874 attr[i] = ""; 875 /** Remove multiline Content, if any */ 876 /** End traversal before END:VCARD */ 877 for (int j = i+1; j < attr.length - 1; j++) { 878 if (checkValidFilter(attr[j])) { 879 break; 880 } else { 881 /** Continuation of above attribute, remove */ 882 attr[j] = ""; 883 } 884 } 885 } 886 } 887 } 888 889 if((!title) && (vCard.contains("TITLE"))) { 890 for (int i=0; i < attr.length; i++) { 891 if(attr[i].startsWith("TITLE")){ 892 attr[i] = ""; 893 /** Remove multiline Content, if any */ 894 /** End traversal before END:VCARD */ 895 for (int j = i+1; j < attr.length - 1; j++) { 896 if (checkValidFilter(attr[j])) { 897 break; 898 } else { 899 /** Continuation of above attribute, remove */ 900 attr[j] = ""; 901 } 902 } 903 } 904 } 905 } 906 907 if((!org) && (vCard.contains("ORG"))) { 908 for (int i=0; i < attr.length; i++) { 909 if(attr[i].startsWith("ORG")){ 910 attr[i] = ""; 911 /** Remove multiline Content, if any */ 912 /** End traversal before END:VCARD */ 913 for (int j = i+1; j < attr.length - 1; j++) { 914 if (checkValidFilter(attr[j])) { 915 break; 916 } else { 917 /** Continuation of above attribute, remove */ 918 attr[j] = ""; 919 } 920 } 921 } 922 } 923 } 924 925 if((!notes) && (vCard.contains("NOTE"))) { 926 for (int i=0; i < attr.length; i++) { 927 if(attr[i].startsWith("NOTE")){ 928 attr[i] = ""; 929 /** Remove multiline Content, if any */ 930 /** End traversal before END:VCARD */ 931 for (int j = i+1; j < attr.length - 1; j++) { 932 if (checkValidFilter(attr[j])) { 933 break; 934 } else { 935 /** Continuation of above attribute, remove */ 936 attr[j] = ""; 937 } 938 } 939 } 940 } 941 } 942 /*Nickname is not supported in 2.1 version. 943 *Android still ads it for 2.1 with nickname mentioned in lower case, and therefore 944 *we need to check for both cases. 945 */ 946 if(((!nickname) || (vCardType21)) && (vCard.contains("NICKNAME"))) { 947 for (int i=0; i < attr.length; i++) { 948 if(attr[i].startsWith("NICKNAME")){ 949 attr[i] = ""; 950 /** Remove multiline Content, if any */ 951 /** End traversal before END:VCARD */ 952 for (int j = i+1; j < attr.length - 1; j++) { 953 if (checkValidFilter(attr[j])) { 954 break; 955 } else { 956 /** Continuation of above attribute, remove */ 957 attr[j] = ""; 958 } 959 } 960 } 961 } 962 } 963 964 if((!url) && (vCard.contains("URL"))) { 965 for (int i=0; i < attr.length; i++) { 966 if(attr[i].startsWith("URL")){ 967 attr[i] = ""; 968 /** Remove multiline Content, if any */ 969 /** End traversal before END:VCARD */ 970 for (int j = i+1; j < attr.length - 1; j++) { 971 if (checkValidFilter(attr[j])) { 972 break; 973 } else { 974 /** Continuation of above attribute, remove */ 975 attr[j] = ""; 976 } 977 } 978 } 979 } 980 } 981 /*Since PBAP does not have filter bit for IM and SIP, 982 *removing them by default. 983 */ 984 if(vCard.toUpperCase().contains("IM")) { 985 for (int i=0; i < attr.length; i++) { 986 if(attr[i].toUpperCase().contains("IM")){ 987 vCard = vCard.replace(attr[i] + "\n", ""); 988 } 989 } 990 } 991 992 if(vCard.toUpperCase().contains("SIP")) { 993 for (int i=0; i < attr.length; i++) { 994 if(attr[i].toUpperCase().contains("SIP")){ 995 vCard = vCard.replace(attr[i] + "\n", ""); 996 } 997 } 998 } 999 1000 Log.v(TAG, "Tokens after applying filter: "); 1001 1002 for (int i=0; i < attr.length; i++) { 1003 if(!attr[i].equals("")){ 1004 filteredVcard = filteredVcard.concat(attr[i] + "\n"); 1005 } 1006 } 1007 1008 return filteredVcard; 1009 } 1010 } 1011 } 1012