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