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 com.android.bluetooth.R; 36 37 import android.net.Uri; 38 import android.os.Handler; 39 import android.text.TextUtils; 40 import android.util.Log; 41 import android.database.Cursor; 42 import android.content.ContentResolver; 43 import android.content.Context; 44 import android.provider.CallLog; 45 import android.provider.CallLog.Calls; 46 import android.provider.ContactsContract.RawContacts; 47 import android.provider.ContactsContract.PhoneLookup; 48 import android.pim.vcard.VCardComposer; 49 import android.pim.vcard.VCardConfig; 50 import android.pim.vcard.VCardComposer.OneEntryHandler; 51 import android.provider.ContactsContract; 52 import android.provider.ContactsContract.CommonDataKinds; 53 import android.provider.ContactsContract.Contacts; 54 import android.provider.ContactsContract.Data; 55 import android.provider.ContactsContract.CommonDataKinds.Phone; 56 57 import javax.obex.ResponseCodes; 58 import javax.obex.Operation; 59 import java.io.IOException; 60 import java.io.OutputStream; 61 import java.io.Writer; 62 import java.util.ArrayList; 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 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 BluetoothPbapVcardManager(final Context context)112 public BluetoothPbapVcardManager(final Context context) { 113 mContext = context; 114 mResolver = mContext.getContentResolver(); 115 } 116 getOwnerPhoneNumberVcard(final boolean vcardType21)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 getPhonebookSize(final int type)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 getContactsSize()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 getCallHistorySize(final int type)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 loadCallHistoryList(final int type)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 getPhonebookNameList(final int orderByWhat)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 getPhonebookNumberList()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 composeAndSendCallLogVcards(final int type, final Operation op, final int startPoint, final int endPoint, final boolean vcardType21)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 composeAndSendPhonebookVcards(final Operation op, final int startPoint, final int endPoint, final boolean vcardType21, String ownerVCard)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 composeAndSendPhonebookOneVcard(final Operation op, final int offset, final boolean vcardType21, String ownerVCard, int orderByWhat)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 composeAndSendVCards(final Operation op, final String selection, final boolean vcardType21, String ownerVCard, boolean isContacts)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 HandlerForStringBuffer(Operation op, String ownerVCard)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 onInit(Context context)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 onEntryCreated(String vcard)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 onTerminate()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