BluetoothPbapVcardManager.java revision 0995f126a9835d63a87d4c2c985a43004978c94d
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 33package com.android.bluetooth.pbap; 34 35import android.content.ContentResolver; 36import android.content.Context; 37import android.database.Cursor; 38import android.net.Uri; 39import android.pim.vcard.VCardComposer; 40import android.pim.vcard.VCardConfig; 41import android.pim.vcard.VCardComposer.OneEntryHandler; 42import android.provider.CallLog; 43import android.provider.CallLog.Calls; 44import android.provider.ContactsContract.CommonDataKinds; 45import android.provider.ContactsContract.Contacts; 46import android.provider.ContactsContract.Data; 47import android.provider.ContactsContract.CommonDataKinds.Phone; 48import android.provider.ContactsContract.PhoneLookup; 49import android.text.TextUtils; 50import android.util.Log; 51 52import com.android.bluetooth.R; 53 54import java.io.IOException; 55import java.io.OutputStream; 56import java.util.ArrayList; 57 58import javax.obex.ServerOperation; 59import javax.obex.Operation; 60import javax.obex.ResponseCodes; 61 62public class BluetoothPbapVcardManager { 63 private static final String TAG = "BluetoothPbapVcardManager"; 64 65 private static final boolean V = BluetoothPbapService.VERBOSE; 66 67 private ContentResolver mResolver; 68 69 private Context mContext; 70 71 private StringBuilder mVcardResults = null; 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, false); 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 } 188 list.add(name); 189 } 190 } 191 } finally { 192 if (callCursor != null) { 193 callCursor.close(); 194 } 195 } 196 return list; 197 } 198 199 public final ArrayList<String> getPhonebookNameList(final int orderByWhat) { 200 ArrayList<String> nameList = new ArrayList<String>(); 201 nameList.add(BluetoothPbapService.getLocalPhoneName()); 202 203 final Uri myUri = Contacts.CONTENT_URI; 204 Cursor contactCursor = null; 205 try { 206 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 207 if (V) Log.v(TAG, "getPhonebookNameList, order by index"); 208 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 209 null, Contacts._ID); 210 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 211 if (V) Log.v(TAG, "getPhonebookNameList, order by alpha"); 212 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 213 null, Contacts.DISPLAY_NAME); 214 } 215 if (contactCursor != null) { 216 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor 217 .moveToNext()) { 218 String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX); 219 if (TextUtils.isEmpty(name)) { 220 name = mContext.getString(android.R.string.unknownName); 221 } 222 nameList.add(name); 223 } 224 } 225 } finally { 226 if (contactCursor != null) { 227 contactCursor.close(); 228 } 229 } 230 return nameList; 231 } 232 233 public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) { 234 ArrayList<String> nameList = new ArrayList<String>(); 235 236 Cursor contactCursor = null; 237 Uri uri = null; 238 239 if (phoneNumber != null && phoneNumber.length() == 0) { 240 uri = Contacts.CONTENT_URI; 241 } else { 242 uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, 243 Uri.encode(phoneNumber)); 244 } 245 246 try { 247 contactCursor = mResolver.query(uri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 248 null, Contacts._ID); 249 250 if (contactCursor != null) { 251 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor 252 .moveToNext()) { 253 String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX); 254 long id = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 255 if (TextUtils.isEmpty(name)) { 256 name = mContext.getString(android.R.string.unknownName); 257 } 258 if (V) Log.v(TAG, "got name " + name + " by number " + phoneNumber + " @" + id); 259 nameList.add(name); 260 } 261 } 262 } finally { 263 if (contactCursor != null) { 264 contactCursor.close(); 265 } 266 } 267 return nameList; 268 } 269 270 public final int composeAndSendCallLogVcards(final int type, Operation op, 271 final int startPoint, final int endPoint, final boolean vcardType21) { 272 if (startPoint < 1 || startPoint > endPoint) { 273 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 274 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 275 } 276 String typeSelection = BluetoothPbapObexServer.createSelectionPara(type); 277 278 final Uri myUri = CallLog.Calls.CONTENT_URI; 279 final String[] CALLLOG_PROJECTION = new String[] { 280 CallLog.Calls._ID, // 0 281 }; 282 final int ID_COLUMN_INDEX = 0; 283 284 Cursor callsCursor = null; 285 long startPointId = 0; 286 long endPointId = 0; 287 try { 288 // Need test to see if order by _ID is ok here, or by date? 289 callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null, 290 CALLLOG_SORT_ORDER); 291 if (callsCursor != null) { 292 callsCursor.moveToPosition(startPoint - 1); 293 startPointId = callsCursor.getLong(ID_COLUMN_INDEX); 294 if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId); 295 if (startPoint == endPoint) { 296 endPointId = startPointId; 297 } else { 298 callsCursor.moveToPosition(endPoint - 1); 299 endPointId = callsCursor.getLong(ID_COLUMN_INDEX); 300 } 301 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId); 302 } 303 } finally { 304 if (callsCursor != null) { 305 callsCursor.close(); 306 } 307 } 308 309 String recordSelection; 310 if (startPoint == endPoint) { 311 recordSelection = Calls._ID + "=" + startPointId; 312 } else { 313 // The query to call table is by "_id DESC" order, so change 314 // correspondingly. 315 recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<=" 316 + startPointId; 317 } 318 319 String selection; 320 if (typeSelection == null) { 321 selection = recordSelection; 322 } else { 323 selection = "(" + typeSelection + ") AND (" + recordSelection + ")"; 324 } 325 326 if (V) Log.v(TAG, "Call log query selection is: " + selection); 327 328 return composeAndSendVCards(op, selection, vcardType21, null, false); 329 } 330 331 public final int composeAndSendPhonebookVcards(Operation op, final int startPoint, 332 final int endPoint, final boolean vcardType21, String ownerVCard) { 333 if (startPoint < 1 || startPoint > endPoint) { 334 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 335 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 336 } 337 final Uri myUri = Contacts.CONTENT_URI; 338 339 Cursor contactCursor = null; 340 long startPointId = 0; 341 long endPointId = 0; 342 try { 343 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, null, 344 Contacts._ID); 345 if (contactCursor != null) { 346 contactCursor.moveToPosition(startPoint - 1); 347 startPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 348 if (V) Log.v(TAG, "Query startPointId = " + startPointId); 349 if (startPoint == endPoint) { 350 endPointId = startPointId; 351 } else { 352 contactCursor.moveToPosition(endPoint - 1); 353 endPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 354 } 355 if (V) Log.v(TAG, "Query endPointId = " + endPointId); 356 } 357 } finally { 358 if (contactCursor != null) { 359 contactCursor.close(); 360 } 361 } 362 363 final String selection; 364 if (startPoint == endPoint) { 365 selection = Contacts._ID + "=" + startPointId + " AND " + CLAUSE_ONLY_VISIBLE; 366 } else { 367 selection = Contacts._ID + ">=" + startPointId + " AND " + Contacts._ID + "<=" 368 + endPointId + " AND " + CLAUSE_ONLY_VISIBLE; 369 } 370 371 if (V) Log.v(TAG, "Query selection is: " + selection); 372 373 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true); 374 } 375 376 public final int composeAndSendPhonebookOneVcard(Operation op, final int offset, 377 final boolean vcardType21, String ownerVCard, int orderByWhat) { 378 if (offset < 1) { 379 Log.e(TAG, "Internal error: offset is not correct."); 380 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 381 } 382 final Uri myUri = Contacts.CONTENT_URI; 383 Cursor contactCursor = null; 384 String selection = null; 385 long contactId = 0; 386 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 387 try { 388 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 389 null, Contacts._ID); 390 if (contactCursor != null) { 391 contactCursor.moveToPosition(offset - 1); 392 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 393 if (V) Log.v(TAG, "Query startPointId = " + contactId); 394 } 395 } finally { 396 if (contactCursor != null) { 397 contactCursor.close(); 398 } 399 } 400 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 401 try { 402 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 403 null, Contacts.DISPLAY_NAME); 404 if (contactCursor != null) { 405 contactCursor.moveToPosition(offset - 1); 406 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 407 if (V) Log.v(TAG, "Query startPointId = " + contactId); 408 } 409 } finally { 410 if (contactCursor != null) { 411 contactCursor.close(); 412 } 413 } 414 } else { 415 Log.e(TAG, "Parameter orderByWhat is not supported!"); 416 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 417 } 418 selection = Contacts._ID + "=" + contactId; 419 420 if (V) Log.v(TAG, "Query selection is: " + selection); 421 422 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true); 423 } 424 425 public final int composeAndSendVCards(Operation op, final String selection, 426 final boolean vcardType21, String ownerVCard, boolean isContacts) { 427 long timestamp = 0; 428 if (V) timestamp = System.currentTimeMillis(); 429 430 if (isContacts) { 431 VCardComposer composer = null; 432 try { 433 // Currently only support Generic Vcard 2.1 and 3.0 434 int vcardType; 435 if (vcardType21) { 436 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 437 } else { 438 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 439 } 440 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 441 vcardType |= VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING; 442 443 composer = new VCardComposer(mContext, vcardType, true); 444 composer.addHandler(new HandlerForStringBuffer(op, ownerVCard)); 445 if (!composer.init(Contacts.CONTENT_URI, selection, null, Contacts._ID)) { 446 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 447 } 448 449 while (!composer.isAfterLast()) { 450 if (BluetoothPbapObexServer.sIsAborted) { 451 ((ServerOperation)op).isAborted = true; 452 BluetoothPbapObexServer.sIsAborted = false; 453 break; 454 } 455 if (!composer.createOneEntry()) { 456 Log.e(TAG, "Failed to read a contact. Error reason: " 457 + composer.getErrorReason()); 458 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 459 } 460 } 461 } finally { 462 if (composer != null) { 463 composer.terminate(); 464 } 465 } 466 } else { // CallLog 467 BluetoothPbapCallLogComposer composer = null; 468 try { 469 composer = new BluetoothPbapCallLogComposer(mContext, true); 470 composer.addHandler(new HandlerForStringBuffer(op, ownerVCard)); 471 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, 472 CALLLOG_SORT_ORDER)) { 473 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 474 } 475 476 while (!composer.isAfterLast()) { 477 if (BluetoothPbapObexServer.sIsAborted) { 478 ((ServerOperation)op).isAborted = true; 479 BluetoothPbapObexServer.sIsAborted = false; 480 break; 481 } 482 if (!composer.createOneEntry()) { 483 Log.e(TAG, "Failed to read a contact. Error reason: " 484 + composer.getErrorReason()); 485 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 486 } 487 } 488 } finally { 489 if (composer != null) { 490 composer.terminate(); 491 } 492 } 493 } 494 495 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 496 + (System.currentTimeMillis() - timestamp) + " ms"); 497 498 return ResponseCodes.OBEX_HTTP_OK; 499 } 500 501 /** 502 * Handler to emit VCard String to PCE once size grow to maxPacketSize. 503 */ 504 public class HandlerForStringBuffer implements OneEntryHandler { 505 private Operation operation; 506 507 private OutputStream outputStream; 508 509 private int maxPacketSize; 510 511 private String phoneOwnVCard = null; 512 513 public HandlerForStringBuffer(Operation op, String ownerVCard) { 514 operation = op; 515 maxPacketSize = operation.getMaxPacketSize(); 516 if (V) Log.v(TAG, "getMaxPacketSize() = " + maxPacketSize); 517 if (ownerVCard != null) { 518 phoneOwnVCard = ownerVCard; 519 if (V) Log.v(TAG, "phone own number vcard:"); 520 if (V) Log.v(TAG, phoneOwnVCard); 521 } 522 } 523 524 public boolean onInit(Context context) { 525 try { 526 outputStream = operation.openOutputStream(); 527 mVcardResults = new StringBuilder(); 528 if (phoneOwnVCard != null) { 529 mVcardResults.append(phoneOwnVCard); 530 } 531 } catch (IOException e) { 532 Log.e(TAG, "open outputstrem failed" + e.toString()); 533 return false; 534 } 535 if (V) Log.v(TAG, "openOutputStream() ok."); 536 return true; 537 } 538 539 public boolean onEntryCreated(String vcard) { 540 int vcardLen = vcard.length(); 541 if (V) Log.v(TAG, "The length of this vcard is: " + vcardLen); 542 543 mVcardResults.append(vcard); 544 int vcardByteLen = mVcardResults.toString().getBytes().length; 545 if (V) Log.v(TAG, "The byte length of this vcardResults is: " + vcardByteLen); 546 547 if (vcardByteLen >= maxPacketSize) { 548 long timestamp = 0; 549 int position = 0; 550 551 // Need while loop to handle the big vcard case 552 while (!BluetoothPbapObexServer.sIsAborted 553 && position < (vcardByteLen - maxPacketSize)) { 554 if (V) timestamp = System.currentTimeMillis(); 555 556 String subStr = mVcardResults.toString().substring(position, 557 position + maxPacketSize); 558 try { 559 outputStream.write(subStr.getBytes(), 0, maxPacketSize); 560 } catch (IOException e) { 561 Log.e(TAG, "write outputstrem failed" + e.toString()); 562 return false; 563 } 564 if (V) Log.v(TAG, "Sending vcard String " + maxPacketSize + " bytes took " 565 + (System.currentTimeMillis() - timestamp) + " ms"); 566 567 position += maxPacketSize; 568 } 569 mVcardResults.delete(0, position); 570 } 571 return true; 572 } 573 574 public void onTerminate() { 575 // Send out last packet 576 byte[] lastBytes = mVcardResults.toString().getBytes(); 577 try { 578 outputStream.write(lastBytes, 0, lastBytes.length); 579 } catch (IOException e) { 580 Log.e(TAG, "write outputstrem failed" + e.toString()); 581 } 582 if (V) Log.v(TAG, "Last packet sent out, sending process complete!"); 583 584 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 585 if (V) Log.v(TAG, "CloseStream failed!"); 586 } else { 587 if (V) Log.v(TAG, "CloseStream ok!"); 588 } 589 } 590 } 591} 592