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