1 /* 2 * Copyright (c) 2008-2009, Motorola, Inc. 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * - Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * - Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * - Neither the name of the Motorola, Inc. nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 * POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.android.bluetooth.pbap; 34 35 import android.content.ContentResolver; 36 import android.content.Context; 37 import android.database.Cursor; 38 import android.net.Uri; 39 import android.provider.CallLog; 40 import android.provider.CallLog.Calls; 41 import android.provider.ContactsContract.CommonDataKinds; 42 import android.provider.ContactsContract.Contacts; 43 import android.provider.ContactsContract.Data; 44 import android.provider.ContactsContract.CommonDataKinds.Phone; 45 import android.provider.ContactsContract.PhoneLookup; 46 import android.telephony.PhoneNumberUtils; 47 import android.text.TextUtils; 48 import android.util.Log; 49 50 import com.android.bluetooth.R; 51 import com.android.vcard.VCardComposer; 52 import com.android.vcard.VCardConfig; 53 import com.android.internal.telephony.CallerInfo; 54 import com.android.vcard.VCardPhoneNumberTranslationCallback; 55 56 import java.io.IOException; 57 import java.io.OutputStream; 58 import java.util.ArrayList; 59 60 import javax.obex.ServerOperation; 61 import javax.obex.Operation; 62 import javax.obex.ResponseCodes; 63 64 public class BluetoothPbapVcardManager { 65 private static final String TAG = "BluetoothPbapVcardManager"; 66 67 private static final boolean V = BluetoothPbapService.VERBOSE; 68 69 private ContentResolver mResolver; 70 71 private Context mContext; 72 73 static final String[] PHONES_PROJECTION = new String[] { 74 Data._ID, // 0 75 CommonDataKinds.Phone.TYPE, // 1 76 CommonDataKinds.Phone.LABEL, // 2 77 CommonDataKinds.Phone.NUMBER, // 3 78 Contacts.DISPLAY_NAME, // 4 79 }; 80 81 private static final int PHONE_NUMBER_COLUMN_INDEX = 3; 82 83 static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC"; 84 85 static final String[] CONTACTS_PROJECTION = new String[] { 86 Contacts._ID, // 0 87 Contacts.DISPLAY_NAME, // 1 88 }; 89 90 static final int CONTACTS_ID_COLUMN_INDEX = 0; 91 92 static final int CONTACTS_NAME_COLUMN_INDEX = 1; 93 94 // call histories use dynamic handles, and handles should order by date; the 95 // most recently one should be the first handle. In table "calls", _id and 96 // date are consistent in ordering, to implement simply, we sort by _id 97 // here. 98 static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC"; 99 100 private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; 101 102 public BluetoothPbapVcardManager(final Context context) { 103 mContext = context; 104 mResolver = mContext.getContentResolver(); 105 } 106 107 public final String getOwnerPhoneNumberVcard(final boolean vcardType21) { 108 BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext); 109 String name = BluetoothPbapService.getLocalPhoneName(); 110 String number = BluetoothPbapService.getLocalPhoneNum(); 111 String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number, 112 vcardType21); 113 return vcard; 114 } 115 116 public final int getPhonebookSize(final int type) { 117 int size; 118 switch (type) { 119 case BluetoothPbapObexServer.ContentType.PHONEBOOK: 120 size = getContactsSize(); 121 break; 122 default: 123 size = getCallHistorySize(type); 124 break; 125 } 126 if (V) Log.v(TAG, "getPhonebookSzie size = " + size + " type = " + type); 127 return size; 128 } 129 130 public final int getContactsSize() { 131 final Uri myUri = Contacts.CONTENT_URI; 132 int size = 0; 133 Cursor contactCursor = null; 134 try { 135 contactCursor = mResolver.query(myUri, null, CLAUSE_ONLY_VISIBLE, null, null); 136 if (contactCursor != null) { 137 size = contactCursor.getCount() + 1; // always has the 0.vcf 138 } 139 } finally { 140 if (contactCursor != null) { 141 contactCursor.close(); 142 } 143 } 144 return size; 145 } 146 147 public final int getCallHistorySize(final int type) { 148 final Uri myUri = CallLog.Calls.CONTENT_URI; 149 String selection = BluetoothPbapObexServer.createSelectionPara(type); 150 int size = 0; 151 Cursor callCursor = null; 152 try { 153 callCursor = mResolver.query(myUri, null, selection, null, 154 CallLog.Calls.DEFAULT_SORT_ORDER); 155 if (callCursor != null) { 156 size = callCursor.getCount(); 157 } 158 } finally { 159 if (callCursor != null) { 160 callCursor.close(); 161 } 162 } 163 return size; 164 } 165 166 public final ArrayList<String> loadCallHistoryList(final int type) { 167 final Uri myUri = CallLog.Calls.CONTENT_URI; 168 String selection = BluetoothPbapObexServer.createSelectionPara(type); 169 String[] projection = new String[] { 170 Calls.NUMBER, Calls.CACHED_NAME 171 }; 172 final int CALLS_NUMBER_COLUMN_INDEX = 0; 173 final int CALLS_NAME_COLUMN_INDEX = 1; 174 175 Cursor callCursor = null; 176 ArrayList<String> list = new ArrayList<String>(); 177 try { 178 callCursor = mResolver.query(myUri, projection, selection, null, 179 CALLLOG_SORT_ORDER); 180 if (callCursor != null) { 181 for (callCursor.moveToFirst(); !callCursor.isAfterLast(); 182 callCursor.moveToNext()) { 183 String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX); 184 if (TextUtils.isEmpty(name)) { 185 // name not found, use number instead 186 name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX); 187 if (CallerInfo.UNKNOWN_NUMBER.equals(name) || 188 CallerInfo.PRIVATE_NUMBER.equals(name) || 189 CallerInfo.PAYPHONE_NUMBER.equals(name)) { 190 name = mContext.getString(R.string.unknownNumber); 191 } 192 } 193 list.add(name); 194 } 195 } 196 } finally { 197 if (callCursor != null) { 198 callCursor.close(); 199 } 200 } 201 return list; 202 } 203 204 public final ArrayList<String> getPhonebookNameList(final int orderByWhat) { 205 ArrayList<String> nameList = new ArrayList<String>(); 206 nameList.add(BluetoothPbapService.getLocalPhoneName()); 207 208 final Uri myUri = Contacts.CONTENT_URI; 209 Cursor contactCursor = null; 210 try { 211 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 212 if (V) Log.v(TAG, "getPhonebookNameList, order by index"); 213 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 214 null, Contacts._ID); 215 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 216 if (V) Log.v(TAG, "getPhonebookNameList, order by alpha"); 217 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 218 null, Contacts.DISPLAY_NAME); 219 } 220 if (contactCursor != null) { 221 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor 222 .moveToNext()) { 223 String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX); 224 if (TextUtils.isEmpty(name)) { 225 name = mContext.getString(android.R.string.unknownName); 226 } 227 nameList.add(name); 228 } 229 } 230 } finally { 231 if (contactCursor != null) { 232 contactCursor.close(); 233 } 234 } 235 return nameList; 236 } 237 238 public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) { 239 ArrayList<String> nameList = new ArrayList<String>(); 240 241 Cursor contactCursor = null; 242 Uri uri = null; 243 244 if (phoneNumber != null && phoneNumber.length() == 0) { 245 uri = Contacts.CONTENT_URI; 246 } else { 247 uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, 248 Uri.encode(phoneNumber)); 249 } 250 251 try { 252 contactCursor = mResolver.query(uri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 253 null, Contacts._ID); 254 255 if (contactCursor != null) { 256 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor 257 .moveToNext()) { 258 String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX); 259 long id = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 260 if (TextUtils.isEmpty(name)) { 261 name = mContext.getString(android.R.string.unknownName); 262 } 263 if (V) Log.v(TAG, "got name " + name + " by number " + phoneNumber + " @" + id); 264 nameList.add(name); 265 } 266 } 267 } finally { 268 if (contactCursor != null) { 269 contactCursor.close(); 270 } 271 } 272 return nameList; 273 } 274 275 public final int composeAndSendCallLogVcards(final int type, Operation op, 276 final int startPoint, final int endPoint, final boolean vcardType21) { 277 if (startPoint < 1 || startPoint > endPoint) { 278 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 279 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 280 } 281 String typeSelection = BluetoothPbapObexServer.createSelectionPara(type); 282 283 final Uri myUri = CallLog.Calls.CONTENT_URI; 284 final String[] CALLLOG_PROJECTION = new String[] { 285 CallLog.Calls._ID, // 0 286 }; 287 final int ID_COLUMN_INDEX = 0; 288 289 Cursor callsCursor = null; 290 long startPointId = 0; 291 long endPointId = 0; 292 try { 293 // Need test to see if order by _ID is ok here, or by date? 294 callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null, 295 CALLLOG_SORT_ORDER); 296 if (callsCursor != null) { 297 callsCursor.moveToPosition(startPoint - 1); 298 startPointId = callsCursor.getLong(ID_COLUMN_INDEX); 299 if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId); 300 if (startPoint == endPoint) { 301 endPointId = startPointId; 302 } else { 303 callsCursor.moveToPosition(endPoint - 1); 304 endPointId = callsCursor.getLong(ID_COLUMN_INDEX); 305 } 306 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId); 307 } 308 } finally { 309 if (callsCursor != null) { 310 callsCursor.close(); 311 } 312 } 313 314 String recordSelection; 315 if (startPoint == endPoint) { 316 recordSelection = Calls._ID + "=" + startPointId; 317 } else { 318 // The query to call table is by "_id DESC" order, so change 319 // correspondingly. 320 recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<=" 321 + startPointId; 322 } 323 324 String selection; 325 if (typeSelection == null) { 326 selection = recordSelection; 327 } else { 328 selection = "(" + typeSelection + ") AND (" + recordSelection + ")"; 329 } 330 331 if (V) Log.v(TAG, "Call log query selection is: " + selection); 332 333 return composeAndSendVCards(op, selection, vcardType21, null, false); 334 } 335 336 public final int composeAndSendPhonebookVcards(Operation op, final int startPoint, 337 final int endPoint, final boolean vcardType21, String ownerVCard) { 338 if (startPoint < 1 || startPoint > endPoint) { 339 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 340 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 341 } 342 final Uri myUri = Contacts.CONTENT_URI; 343 344 Cursor contactCursor = null; 345 long startPointId = 0; 346 long endPointId = 0; 347 try { 348 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, null, 349 Contacts._ID); 350 if (contactCursor != null) { 351 contactCursor.moveToPosition(startPoint - 1); 352 startPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 353 if (V) Log.v(TAG, "Query startPointId = " + startPointId); 354 if (startPoint == endPoint) { 355 endPointId = startPointId; 356 } else { 357 contactCursor.moveToPosition(endPoint - 1); 358 endPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 359 } 360 if (V) Log.v(TAG, "Query endPointId = " + endPointId); 361 } 362 } finally { 363 if (contactCursor != null) { 364 contactCursor.close(); 365 } 366 } 367 368 final String selection; 369 if (startPoint == endPoint) { 370 selection = Contacts._ID + "=" + startPointId + " AND " + CLAUSE_ONLY_VISIBLE; 371 } else { 372 selection = Contacts._ID + ">=" + startPointId + " AND " + Contacts._ID + "<=" 373 + endPointId + " AND " + CLAUSE_ONLY_VISIBLE; 374 } 375 376 if (V) Log.v(TAG, "Query selection is: " + selection); 377 378 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true); 379 } 380 381 public final int composeAndSendPhonebookOneVcard(Operation op, final int offset, 382 final boolean vcardType21, String ownerVCard, int orderByWhat) { 383 if (offset < 1) { 384 Log.e(TAG, "Internal error: offset is not correct."); 385 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 386 } 387 final Uri myUri = Contacts.CONTENT_URI; 388 Cursor contactCursor = null; 389 String selection = null; 390 long contactId = 0; 391 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 392 try { 393 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 394 null, Contacts._ID); 395 if (contactCursor != null) { 396 contactCursor.moveToPosition(offset - 1); 397 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 398 if (V) Log.v(TAG, "Query startPointId = " + contactId); 399 } 400 } finally { 401 if (contactCursor != null) { 402 contactCursor.close(); 403 } 404 } 405 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 406 try { 407 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 408 null, Contacts.DISPLAY_NAME); 409 if (contactCursor != null) { 410 contactCursor.moveToPosition(offset - 1); 411 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 412 if (V) Log.v(TAG, "Query startPointId = " + contactId); 413 } 414 } finally { 415 if (contactCursor != null) { 416 contactCursor.close(); 417 } 418 } 419 } else { 420 Log.e(TAG, "Parameter orderByWhat is not supported!"); 421 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 422 } 423 selection = Contacts._ID + "=" + contactId; 424 425 if (V) Log.v(TAG, "Query selection is: " + selection); 426 427 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true); 428 } 429 430 public final int composeAndSendVCards(Operation op, final String selection, 431 final boolean vcardType21, String ownerVCard, boolean isContacts) { 432 long timestamp = 0; 433 if (V) timestamp = System.currentTimeMillis(); 434 435 if (isContacts) { 436 VCardComposer composer = null; 437 HandlerForStringBuffer buffer = null; 438 try { 439 // Currently only support Generic Vcard 2.1 and 3.0 440 int vcardType; 441 if (vcardType21) { 442 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 443 } else { 444 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 445 } 446 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 447 448 composer = new VCardComposer(mContext, vcardType, true); 449 // BT does want PAUSE/WAIT conversion while it doesn't want the other formatting 450 // done by vCard library by default. 451 composer.setPhoneNumberTranslationCallback( 452 new VCardPhoneNumberTranslationCallback() { 453 public String onValueReceived( 454 String rawValue, int type, String label, boolean isPrimary) { 455 // 'p' and 'w' are the standard characters for pause and wait 456 // (see RFC 3601) 457 // so use those when exporting phone numbers via vCard. 458 String numberWithControlSequence = rawValue 459 .replace(PhoneNumberUtils.PAUSE, 'p') 460 .replace(PhoneNumberUtils.WAIT, 'w'); 461 return numberWithControlSequence; 462 } 463 }); 464 buffer = new HandlerForStringBuffer(op, ownerVCard); 465 if (!composer.init(Contacts.CONTENT_URI, selection, null, Contacts._ID) || 466 !buffer.onInit(mContext)) { 467 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 468 } 469 470 while (!composer.isAfterLast()) { 471 if (BluetoothPbapObexServer.sIsAborted) { 472 ((ServerOperation)op).isAborted = true; 473 BluetoothPbapObexServer.sIsAborted = false; 474 break; 475 } 476 String vcard = composer.createOneEntry(); 477 if (vcard == null) { 478 Log.e(TAG, "Failed to read a contact. Error reason: " 479 + composer.getErrorReason()); 480 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 481 } 482 if (!buffer.onEntryCreated(vcard)) { 483 // onEntryCreate() already emits error. 484 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 485 } 486 } 487 } finally { 488 if (composer != null) { 489 composer.terminate(); 490 } 491 if (buffer != null) { 492 buffer.onTerminate(); 493 } 494 } 495 } else { // CallLog 496 BluetoothPbapCallLogComposer composer = null; 497 HandlerForStringBuffer buffer = null; 498 try { 499 500 composer = new BluetoothPbapCallLogComposer(mContext); 501 buffer = new HandlerForStringBuffer(op, ownerVCard); 502 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, 503 CALLLOG_SORT_ORDER) || 504 !buffer.onInit(mContext)) { 505 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 506 } 507 508 while (!composer.isAfterLast()) { 509 if (BluetoothPbapObexServer.sIsAborted) { 510 ((ServerOperation)op).isAborted = true; 511 BluetoothPbapObexServer.sIsAborted = false; 512 break; 513 } 514 String vcard = composer.createOneEntry(vcardType21); 515 if (vcard == null) { 516 Log.e(TAG, "Failed to read a contact. Error reason: " 517 + composer.getErrorReason()); 518 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 519 } 520 buffer.onEntryCreated(vcard); 521 } 522 } finally { 523 if (composer != null) { 524 composer.terminate(); 525 } 526 if (buffer != null) { 527 buffer.onTerminate(); 528 } 529 } 530 } 531 532 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 533 + (System.currentTimeMillis() - timestamp) + " ms"); 534 535 return ResponseCodes.OBEX_HTTP_OK; 536 } 537 538 /** 539 * Handler to emit vCards to PCE. 540 */ 541 public class HandlerForStringBuffer { 542 private Operation operation; 543 544 private OutputStream outputStream; 545 546 private String phoneOwnVCard = null; 547 548 public HandlerForStringBuffer(Operation op, String ownerVCard) { 549 operation = op; 550 if (ownerVCard != null) { 551 phoneOwnVCard = ownerVCard; 552 if (V) Log.v(TAG, "phone own number vcard:"); 553 if (V) Log.v(TAG, phoneOwnVCard); 554 } 555 } 556 557 private boolean write(String vCard) { 558 try { 559 if (vCard != null) { 560 outputStream.write(vCard.getBytes()); 561 return true; 562 } 563 } catch (IOException e) { 564 Log.e(TAG, "write outputstrem failed" + e.toString()); 565 } 566 return false; 567 } 568 569 public boolean onInit(Context context) { 570 try { 571 outputStream = operation.openOutputStream(); 572 if (phoneOwnVCard != null) { 573 return write(phoneOwnVCard); 574 } 575 return true; 576 } catch (IOException e) { 577 Log.e(TAG, "open outputstrem failed" + e.toString()); 578 } 579 return false; 580 } 581 582 public boolean onEntryCreated(String vcard) { 583 return write(vcard); 584 } 585 586 public void onTerminate() { 587 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 588 if (V) Log.v(TAG, "CloseStream failed!"); 589 } else { 590 if (V) Log.v(TAG, "CloseStream ok!"); 591 } 592 } 593 } 594 } 595