BluetoothPbapVcardManager.java revision f3d6b25c0ea3b2bcd01ab457967c7b40ccd20b5b
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.provider.CallLog; 40import android.provider.CallLog.Calls; 41import android.provider.ContactsContract.CommonDataKinds; 42import android.provider.ContactsContract.Contacts; 43import android.provider.ContactsContract.Data; 44import android.provider.ContactsContract.CommonDataKinds.Phone; 45import android.text.TextUtils; 46import android.util.Log; 47 48import com.android.bluetooth.R; 49import com.android.vcard.VCardComposer; 50import com.android.vcard.VCardConfig; 51import com.android.vcard.VCardComposer.OneEntryHandler; 52 53import java.io.IOException; 54import java.io.OutputStream; 55import java.util.ArrayList; 56 57import javax.obex.ServerOperation; 58import javax.obex.Operation; 59import javax.obex.ResponseCodes; 60 61public class BluetoothPbapVcardManager { 62 private static final String TAG = "BluetoothPbapVcardManager"; 63 64 private static final boolean V = BluetoothPbapService.VERBOSE; 65 66 private ContentResolver mResolver; 67 68 private Context mContext; 69 70 private StringBuilder mVcardResults = null; 71 72 static final String[] PHONES_PROJECTION = new String[] { 73 Data._ID, // 0 74 CommonDataKinds.Phone.TYPE, // 1 75 CommonDataKinds.Phone.LABEL, // 2 76 CommonDataKinds.Phone.NUMBER, // 3 77 Contacts.DISPLAY_NAME, // 4 78 }; 79 80 private static final int ID_COLUMN_INDEX = 0; 81 82 private static final int PHONE_TYPE_COLUMN_INDEX = 1; 83 84 private static final int PHONE_LABEL_COLUMN_INDEX = 2; 85 86 private static final int PHONE_NUMBER_COLUMN_INDEX = 3; 87 88 private static final int CONTACTS_DISPLAY_NAME_COLUMN_INDEX = 4; 89 90 static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC"; 91 92 static final String[] CONTACTS_PROJECTION = new String[] { 93 Contacts._ID, // 0 94 Contacts.DISPLAY_NAME, // 1 95 }; 96 97 static final int CONTACTS_ID_COLUMN_INDEX = 0; 98 99 static final int CONTACTS_NAME_COLUMN_INDEX = 1; 100 101 // call histories use dynamic handles, and handles should order by date; the 102 // most recently one should be the first handle. In table "calls", _id and 103 // date are consistent in ordering, to implement simply, we sort by _id 104 // here. 105 static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC"; 106 107 private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; 108 109 public BluetoothPbapVcardManager(final Context context) { 110 mContext = context; 111 mResolver = mContext.getContentResolver(); 112 } 113 114 public final String getOwnerPhoneNumberVcard(final boolean vcardType21) { 115 BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext, false); 116 String name = BluetoothPbapService.getLocalPhoneName(); 117 String number = BluetoothPbapService.getLocalPhoneNum(); 118 String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number, 119 vcardType21); 120 return vcard; 121 } 122 123 public final int getPhonebookSize(final int type) { 124 int size; 125 switch (type) { 126 case BluetoothPbapObexServer.ContentType.PHONEBOOK: 127 size = getContactsSize(); 128 break; 129 default: 130 size = getCallHistorySize(type); 131 break; 132 } 133 if (V) Log.v(TAG, "getPhonebookSzie size = " + size + " type = " + type); 134 return size; 135 } 136 137 public final int getContactsSize() { 138 final Uri myUri = Contacts.CONTENT_URI; 139 int size = 0; 140 Cursor contactCursor = null; 141 try { 142 contactCursor = mResolver.query(myUri, null, CLAUSE_ONLY_VISIBLE, null, null); 143 if (contactCursor != null) { 144 size = contactCursor.getCount() + 1; // always has the 0.vcf 145 } 146 } finally { 147 if (contactCursor != null) { 148 contactCursor.close(); 149 } 150 } 151 return size; 152 } 153 154 public final int getCallHistorySize(final int type) { 155 final Uri myUri = CallLog.Calls.CONTENT_URI; 156 String selection = BluetoothPbapObexServer.createSelectionPara(type); 157 int size = 0; 158 Cursor callCursor = null; 159 try { 160 callCursor = mResolver.query(myUri, null, selection, null, 161 CallLog.Calls.DEFAULT_SORT_ORDER); 162 if (callCursor != null) { 163 size = callCursor.getCount(); 164 } 165 } finally { 166 if (callCursor != null) { 167 callCursor.close(); 168 } 169 } 170 return size; 171 } 172 173 public final ArrayList<String> loadCallHistoryList(final int type) { 174 final Uri myUri = CallLog.Calls.CONTENT_URI; 175 String selection = BluetoothPbapObexServer.createSelectionPara(type); 176 String[] projection = new String[] { 177 Calls.NUMBER, Calls.CACHED_NAME 178 }; 179 final int CALLS_NUMBER_COLUMN_INDEX = 0; 180 final int CALLS_NAME_COLUMN_INDEX = 1; 181 182 Cursor callCursor = null; 183 ArrayList<String> list = new ArrayList<String>(); 184 try { 185 callCursor = mResolver.query(myUri, projection, selection, null, 186 CALLLOG_SORT_ORDER); 187 if (callCursor != null) { 188 for (callCursor.moveToFirst(); !callCursor.isAfterLast(); 189 callCursor.moveToNext()) { 190 String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX); 191 if (TextUtils.isEmpty(name)) { 192 // name not found,use number instead 193 name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX); 194 } 195 list.add(name); 196 } 197 } 198 } finally { 199 if (callCursor != null) { 200 callCursor.close(); 201 } 202 } 203 return list; 204 } 205 206 public final ArrayList<String> getPhonebookNameList(final int orderByWhat) { 207 ArrayList<String> nameList = new ArrayList<String>(); 208 nameList.add(BluetoothPbapService.getLocalPhoneName()); 209 210 final Uri myUri = Contacts.CONTENT_URI; 211 Cursor contactCursor = null; 212 try { 213 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 214 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 215 null, Contacts._ID); 216 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 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> getPhonebookNumberList() { 239 ArrayList<String> numberList = new ArrayList<String>(); 240 numberList.add(BluetoothPbapService.getLocalPhoneNum()); 241 242 final Uri myUri = Phone.CONTENT_URI; 243 Cursor phoneCursor = null; 244 try { 245 phoneCursor = mResolver.query(myUri, PHONES_PROJECTION, CLAUSE_ONLY_VISIBLE, null, 246 SORT_ORDER_PHONE_NUMBER); 247 if (phoneCursor != null) { 248 for (phoneCursor.moveToFirst(); !phoneCursor.isAfterLast(); phoneCursor 249 .moveToNext()) { 250 String number = phoneCursor.getString(PHONE_NUMBER_COLUMN_INDEX); 251 if (TextUtils.isEmpty(number)) { 252 number = mContext.getString(R.string.defaultnumber); 253 } 254 numberList.add(number); 255 } 256 } 257 } finally { 258 if (phoneCursor != null) { 259 phoneCursor.close(); 260 } 261 } 262 return numberList; 263 } 264 265 public final int composeAndSendCallLogVcards(final int type, Operation op, 266 final int startPoint, final int endPoint, final boolean vcardType21) { 267 if (startPoint < 1 || startPoint > endPoint) { 268 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 269 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 270 } 271 String typeSelection = BluetoothPbapObexServer.createSelectionPara(type); 272 273 final Uri myUri = CallLog.Calls.CONTENT_URI; 274 final String[] CALLLOG_PROJECTION = new String[] { 275 CallLog.Calls._ID, // 0 276 }; 277 final int ID_COLUMN_INDEX = 0; 278 279 Cursor callsCursor = null; 280 long startPointId = 0; 281 long endPointId = 0; 282 try { 283 // Need test to see if order by _ID is ok here, or by date? 284 callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null, 285 CALLLOG_SORT_ORDER); 286 if (callsCursor != null) { 287 callsCursor.moveToPosition(startPoint - 1); 288 startPointId = callsCursor.getLong(ID_COLUMN_INDEX); 289 if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId); 290 if (startPoint == endPoint) { 291 endPointId = startPointId; 292 } else { 293 callsCursor.moveToPosition(endPoint - 1); 294 endPointId = callsCursor.getLong(ID_COLUMN_INDEX); 295 } 296 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId); 297 } 298 } finally { 299 if (callsCursor != null) { 300 callsCursor.close(); 301 } 302 } 303 304 String recordSelection; 305 if (startPoint == endPoint) { 306 recordSelection = Calls._ID + "=" + startPointId; 307 } else { 308 // The query to call table is by "_id DESC" order, so change 309 // correspondingly. 310 recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<=" 311 + startPointId; 312 } 313 314 String selection; 315 if (typeSelection == null) { 316 selection = recordSelection; 317 } else { 318 selection = "(" + typeSelection + ") AND (" + recordSelection + ")"; 319 } 320 321 if (V) Log.v(TAG, "Call log query selection is: " + selection); 322 323 return composeAndSendVCards(op, selection, vcardType21, null, false); 324 } 325 326 public final int composeAndSendPhonebookVcards(Operation op, final int startPoint, 327 final int endPoint, final boolean vcardType21, String ownerVCard) { 328 if (startPoint < 1 || startPoint > endPoint) { 329 Log.e(TAG, "internal error: startPoint or endPoint is not correct."); 330 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 331 } 332 final Uri myUri = Contacts.CONTENT_URI; 333 334 Cursor contactCursor = null; 335 long startPointId = 0; 336 long endPointId = 0; 337 try { 338 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, null, 339 Contacts._ID); 340 if (contactCursor != null) { 341 contactCursor.moveToPosition(startPoint - 1); 342 startPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 343 if (V) Log.v(TAG, "Query startPointId = " + startPointId); 344 if (startPoint == endPoint) { 345 endPointId = startPointId; 346 } else { 347 contactCursor.moveToPosition(endPoint - 1); 348 endPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 349 } 350 if (V) Log.v(TAG, "Query endPointId = " + endPointId); 351 } 352 } finally { 353 if (contactCursor != null) { 354 contactCursor.close(); 355 } 356 } 357 358 final String selection; 359 if (startPoint == endPoint) { 360 selection = Contacts._ID + "=" + startPointId + " AND " + CLAUSE_ONLY_VISIBLE; 361 } else { 362 selection = Contacts._ID + ">=" + startPointId + " AND " + Contacts._ID + "<=" 363 + endPointId + " AND " + CLAUSE_ONLY_VISIBLE; 364 } 365 366 if (V) Log.v(TAG, "Query selection is: " + selection); 367 368 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true); 369 } 370 371 public final int composeAndSendPhonebookOneVcard(Operation op, final int offset, 372 final boolean vcardType21, String ownerVCard, int orderByWhat) { 373 if (offset < 1) { 374 Log.e(TAG, "Internal error: offset is not correct."); 375 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 376 } 377 final Uri myUri = Contacts.CONTENT_URI; 378 Cursor contactCursor = null; 379 String selection = null; 380 long contactId = 0; 381 if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { 382 try { 383 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 384 null, Contacts._ID); 385 if (contactCursor != null) { 386 contactCursor.moveToPosition(offset - 1); 387 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 388 if (V) Log.v(TAG, "Query startPointId = " + contactId); 389 } 390 } finally { 391 if (contactCursor != null) { 392 contactCursor.close(); 393 } 394 } 395 } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { 396 try { 397 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, 398 null, Contacts.DISPLAY_NAME); 399 if (contactCursor != null) { 400 contactCursor.moveToPosition(offset - 1); 401 contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX); 402 if (V) Log.v(TAG, "Query startPointId = " + contactId); 403 } 404 } finally { 405 if (contactCursor != null) { 406 contactCursor.close(); 407 } 408 } 409 } else { 410 Log.e(TAG, "Parameter orderByWhat is not supported!"); 411 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 412 } 413 selection = Contacts._ID + "=" + contactId; 414 415 if (V) Log.v(TAG, "Query selection is: " + selection); 416 417 return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true); 418 } 419 420 public final int composeAndSendVCards(Operation op, final String selection, 421 final boolean vcardType21, String ownerVCard, boolean isContacts) { 422 long timestamp = 0; 423 if (V) timestamp = System.currentTimeMillis(); 424 425 if (isContacts) { 426 VCardComposer composer = null; 427 try { 428 // Currently only support Generic Vcard 2.1 and 3.0 429 int vcardType; 430 if (vcardType21) { 431 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; 432 } else { 433 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; 434 } 435 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 436 vcardType |= VCardConfig.FLAG_REFRAIN_PHONE_NUMBER_FORMATTING; 437 438 composer = new VCardComposer(mContext, vcardType, true); 439 composer.addHandler(new HandlerForStringBuffer(op, ownerVCard)); 440 if (!composer.init(Contacts.CONTENT_URI, selection, null, null)) { 441 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 442 } 443 444 while (!composer.isAfterLast()) { 445 if (BluetoothPbapObexServer.sIsAborted) { 446 ((ServerOperation)op).isAborted = true; 447 BluetoothPbapObexServer.sIsAborted = false; 448 break; 449 } 450 if (!composer.createOneEntry()) { 451 Log.e(TAG, "Failed to read a contact. Error reason: " 452 + composer.getErrorReason()); 453 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 454 } 455 } 456 } finally { 457 if (composer != null) { 458 composer.terminate(); 459 } 460 } 461 } else { // CallLog 462 BluetoothPbapCallLogComposer composer = null; 463 try { 464 composer = new BluetoothPbapCallLogComposer(mContext, true); 465 composer.addHandler(new HandlerForStringBuffer(op, ownerVCard)); 466 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, null)) { 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 if (!composer.createOneEntry()) { 477 Log.e(TAG, "Failed to read a contact. Error reason: " 478 + composer.getErrorReason()); 479 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; 480 } 481 } 482 } finally { 483 if (composer != null) { 484 composer.terminate(); 485 } 486 } 487 } 488 489 if (V) Log.v(TAG, "Total vcard composing and sending out takes " 490 + (System.currentTimeMillis() - timestamp) + " ms"); 491 492 return ResponseCodes.OBEX_HTTP_OK; 493 } 494 495 /** 496 * Handler to emit VCard String to PCE once size grow to maxPacketSize. 497 */ 498 public class HandlerForStringBuffer implements OneEntryHandler { 499 @SuppressWarnings("hiding") 500 private Operation operation; 501 502 private OutputStream outputStream; 503 504 private int maxPacketSize; 505 506 private String phoneOwnVCard = null; 507 508 public HandlerForStringBuffer(Operation op, String ownerVCard) { 509 operation = op; 510 maxPacketSize = operation.getMaxPacketSize(); 511 if (V) Log.v(TAG, "getMaxPacketSize() = " + maxPacketSize); 512 if (ownerVCard != null) { 513 phoneOwnVCard = ownerVCard; 514 if (V) Log.v(TAG, "phone own number vcard:"); 515 if (V) Log.v(TAG, phoneOwnVCard); 516 } 517 } 518 519 public boolean onInit(Context context) { 520 try { 521 outputStream = operation.openOutputStream(); 522 mVcardResults = new StringBuilder(); 523 if (phoneOwnVCard != null) { 524 mVcardResults.append(phoneOwnVCard); 525 } 526 } catch (IOException e) { 527 Log.e(TAG, "open outputstrem failed" + e.toString()); 528 return false; 529 } 530 if (V) Log.v(TAG, "openOutputStream() ok."); 531 return true; 532 } 533 534 public boolean onEntryCreated(String vcard) { 535 int vcardLen = vcard.length(); 536 if (V) Log.v(TAG, "The length of this vcard is: " + vcardLen); 537 538 mVcardResults.append(vcard); 539 int vcardByteLen = mVcardResults.toString().getBytes().length; 540 if (V) Log.v(TAG, "The byte length of this vcardResults is: " + vcardByteLen); 541 542 if (vcardByteLen >= maxPacketSize) { 543 long timestamp = 0; 544 int position = 0; 545 546 // Need while loop to handle the big vcard case 547 while (!BluetoothPbapObexServer.sIsAborted 548 && position < (vcardByteLen - maxPacketSize)) { 549 if (V) timestamp = System.currentTimeMillis(); 550 551 String subStr = mVcardResults.toString().substring(position, 552 position + maxPacketSize); 553 try { 554 outputStream.write(subStr.getBytes(), 0, maxPacketSize); 555 } catch (IOException e) { 556 Log.e(TAG, "write outputstrem failed" + e.toString()); 557 return false; 558 } 559 if (V) Log.v(TAG, "Sending vcard String " + maxPacketSize + " bytes took " 560 + (System.currentTimeMillis() - timestamp) + " ms"); 561 562 position += maxPacketSize; 563 } 564 mVcardResults.delete(0, position); 565 } 566 return true; 567 } 568 569 public void onTerminate() { 570 // Send out last packet 571 byte[] lastBytes = mVcardResults.toString().getBytes(); 572 try { 573 outputStream.write(lastBytes, 0, lastBytes.length); 574 } catch (IOException e) { 575 Log.e(TAG, "write outputstrem failed" + e.toString()); 576 } 577 if (V) Log.v(TAG, "Last packet sent out, sending process complete!"); 578 579 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) { 580 if (V) Log.v(TAG, "CloseStream failed!"); 581 } else { 582 if (V) Log.v(TAG, "CloseStream ok!"); 583 } 584 } 585 } 586} 587